Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions packages/openapi-ts-tests/main/test/3.1.x.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
// This file is auto-generated by @hey-api/openapi-ts

export type * from './types.gen';
Original file line number Diff line number Diff line change
@@ -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<number>
];
/**
* Non-empty array with minItems=3
*/
tags?: [
string,
string,
string,
...Array<string>
];
/**
* Normal array without minItems
*/
categories?: Array<string>;
};
url: '/items';
};

export type GetItemsResponses = {
/**
* Success
*/
200: {
requiredItems?: [
string,
...Array<string>
];
optionalItems?: Array<number>;
fixedItems?: [
boolean,
boolean
];
};
};

export type GetItemsResponse = GetItemsResponses[keyof GetItemsResponses];
11 changes: 11 additions & 0 deletions packages/openapi-ts/src/plugins/@hey-api/typescript/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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]!);
}
Expand Down
1 change: 1 addition & 0 deletions packages/openapi-ts/src/tsc/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
31 changes: 31 additions & 0 deletions packages/openapi-ts/src/tsc/typedef.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
createKeywordTypeNode,
createMappedTypeNode,
createParameterDeclaration,
createRestTypeNode,
createStringLiteral,
createTypeNode,
createTypeParameterDeclaration,
Expand Down Expand Up @@ -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<ts.TypeNode> = [];
for (let i = 0; i < minItems; i++) {
elements.push(typeNode);
}
elements.push(restElement);

const node = ts.factory.createTupleTypeNode(elements);
return maybeNullable({ isNullable, node });
};
3 changes: 3 additions & 0 deletions packages/openapi-ts/src/tsc/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1130,3 +1130,6 @@ export const createTemplateLiteralType = ({
);
return templateLiteralType;
};

export const createRestTypeNode = ({ type }: { type: ts.TypeNode }) =>
ts.factory.createRestTypeNode(type);
15 changes: 15 additions & 0 deletions packages/openapi-ts/src/utils/type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
58 changes: 58 additions & 0 deletions specs/3.1.x/min-items-array.yaml
Original file line number Diff line number Diff line change
@@ -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