Skip to content

Commit cf9cfb7

Browse files
authored
Supports minItems / maxItems (#872)
* Supports minItems / maxItems fix #871 * Add document and help. * Add test
1 parent fb5e199 commit cf9cfb7

17 files changed

+89206
-2
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,7 @@ npx openapi-typescript schema.yaml
159159
| `--default-non-nullable` | | `false` | (optional) Treat schema objects with default values as non-nullable |
160160
| `--prettier-config [location]` | `-c` | | (optional) Path to your custom Prettier configuration for output |
161161
| `--export-type` | | `false` | (optional) Export `type` instead of `interface` |
162+
| `--support-array-length` | | `false` | (optional) Generate tuples using array minItems / maxItems |
162163
| `--raw-schema` | | `false` | Generate TS types from partial schema (e.g. having `components.schema` at the top level) |
163164
| `--version` | | | Force OpenAPI version with `--version 3` or `--version 2` (required for `--raw-schema` when version is unknown) |
164165

bin/cli.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ Options
2626
--prettier-config, -c (optional) specify path to Prettier config file
2727
--raw-schema (optional) Parse as partial schema (raw components)
2828
--export-type (optional) Export type instead of interface
29+
--support-array-length (optional) Generate tuples using array minItems / maxItems
2930
--version (optional) Force schema parsing version
3031
`;
3132

@@ -42,7 +43,7 @@ function errorAndExit(errorMessage) {
4243
const [, , input, ...args] = process.argv;
4344
const flags = parser(args, {
4445
array: ["header"],
45-
boolean: ["defaultNonNullable", "immutableTypes", "rawSchema", "exportType"],
46+
boolean: ["defaultNonNullable", "immutableTypes", "rawSchema", "exportType", "supportArrayLength"],
4647
number: ["version"],
4748
string: ["auth", "header", "headersObject", "httpMethod", "prettierConfig"],
4849
alias: {
@@ -92,6 +93,7 @@ async function generateSchema(pathToSpec) {
9293
httpHeaders,
9394
httpMethod: flags.httpMethod,
9495
exportType: flags.exportType,
96+
supportArrayLength: flags.supportArrayLength,
9597
});
9698

9799
// output

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ async function openapiTS(
4141
immutableTypes: options.immutableTypes || false,
4242
rawSchema: options.rawSchema || false,
4343
version: options.version || 3,
44+
supportArrayLength: options.supportArrayLength,
4445
} as any;
4546

4647
// note: we may be loading many large schemas into memory at once; take care to reuse references without cloning

src/transform/schema.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,31 @@ export function transformSchemaObj(node: any, options: TransformSchemaObjOptions
189189
if (Array.isArray(node.items)) {
190190
output += `${readonly}${tsTupleOf(node.items.map((node: any) => transformSchemaObj(node, options)))}`;
191191
} else {
192-
output += `${readonly}${tsArrayOf(node.items ? transformSchemaObj(node.items as any, options) : "unknown")}`;
192+
const minItems: number = Number.isInteger(node.minItems) && node.minItems >= 0 ? node.minItems : 0;
193+
const maxItems: number | undefined =
194+
Number.isInteger(node.maxItems) && node.maxItems >= 0 && minItems <= node.maxItems
195+
? node.maxItems
196+
: undefined;
197+
198+
const estimateCodeSize =
199+
maxItems === undefined ? minItems : (maxItems * (maxItems + 1) - minItems * (minItems - 1)) / 2;
200+
const items = node.items ? transformSchemaObj(node.items as any, options) : "unknown";
201+
if ((minItems !== 0 || maxItems !== undefined) && options.supportArrayLength && estimateCodeSize < 30) {
202+
if (maxItems === undefined) {
203+
output += `${readonly}${tsTupleOf([
204+
...Array.from({ length: minItems }).map(() => items),
205+
`...${tsArrayOf(items)}`,
206+
])}`;
207+
} else {
208+
output += tsUnionOf(
209+
Array.from({ length: maxItems - minItems + 1 })
210+
.map((_, i) => i + minItems)
211+
.map((n) => `${readonly}${tsTupleOf(Array.from({ length: n }).map(() => items))}`)
212+
);
213+
}
214+
} else {
215+
output += `${readonly}${tsArrayOf(items)}`;
216+
}
193217
}
194218
break;
195219
}

src/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,10 @@ export interface SwaggerToTSOptions {
155155
* (optional) Export type instead of interface
156156
*/
157157
exportType?: boolean;
158+
/**
159+
* (optional) Generate tuples using array minItems / maxItems
160+
*/
161+
supportArrayLength?: boolean;
158162
}
159163

160164
/** Context passed to all submodules */
@@ -169,4 +173,5 @@ export interface GlobalContext {
169173
namespace?: string;
170174
rawSchema: boolean;
171175
version: number;
176+
supportArrayLength?: boolean;
172177
}

test/core/schema.test.js

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const defaults = {
1111
required: new Set(),
1212
rawSchema: false,
1313
version: 3,
14+
supportArrayLength: false,
1415
};
1516

1617
describe("SchemaObject", () => {
@@ -150,6 +151,34 @@ describe("SchemaObject", () => {
150151
);
151152
});
152153

154+
it("array (supportArrayLength)", () => {
155+
// (same as above test, but with supportArrayLength: true)
156+
const opts = { ...defaults, supportArrayLength: true };
157+
expect(transform({ type: "array", items: { type: "string" } }, opts)).to.equal(`(string)[]`);
158+
expect(transform({ type: "array", items: { type: "string" }, minItems: 1 }, opts)).to.equal(
159+
`[string, ...(string)[]]`
160+
);
161+
expect(transform({ type: "array", items: { type: "string" }, maxItems: 2 }, opts)).to.equal(
162+
`([]) | ([string]) | ([string, string])`
163+
);
164+
expect(transform({ type: "array", items: { type: "string" }, maxItems: 20 }, opts)).to.equal(`(string)[]`);
165+
});
166+
167+
it("array (immutableTypes, supportArrayLength)", () => {
168+
// (same as above test, but with immutableTypes: true, supportArrayLength: true)
169+
const opts = { ...defaults, immutableTypes: true, supportArrayLength: true };
170+
expect(transform({ type: "array", items: { type: "string" } }, opts)).to.equal(`readonly (string)[]`);
171+
expect(transform({ type: "array", items: { type: "string" }, minItems: 1 }, opts)).to.equal(
172+
`readonly [string, ...(string)[]]`
173+
);
174+
expect(transform({ type: "array", items: { type: "string" }, maxItems: 2 }, opts)).to.equal(
175+
`(readonly []) | (readonly [string]) | (readonly [string, string])`
176+
);
177+
expect(transform({ type: "array", items: { type: "string" }, maxItems: 20 }, opts)).to.equal(
178+
`readonly (string)[]`
179+
);
180+
});
181+
153182
it("enum", () => {
154183
const enumBasic = ["Totoro", "Sats'uki", "Mei"]; // note: also tests quotes in enum
155184
expect(
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**
2+
* This file was auto-generated by openapi-typescript.
3+
* Do not make direct changes to the file.
4+
*/
5+
6+
export interface paths {
7+
"/test": {
8+
get: {
9+
responses: {
10+
/** A list of types. */
11+
200: unknown;
12+
};
13+
};
14+
};
15+
}
16+
17+
export interface components {
18+
schemas: {
19+
/** @description Enum with null and nullable */
20+
MyType: {
21+
/** @enum {string|null} */
22+
myEnumTestFieldNullable?: ("foo" | "bar" | null) | null;
23+
/** @enum {string|null} */
24+
myEnumTestField?: ("foo" | "bar" | null) | null;
25+
/** @constant */
26+
myConstTestField?: "constant-value";
27+
/** @constant */
28+
myConstTestFieldNullable?: 4 | null;
29+
};
30+
};
31+
}
32+
33+
export interface operations {}
34+
35+
export interface external {}

0 commit comments

Comments
 (0)