Skip to content

Supports minItems / maxItems #872

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Feb 15, 2022
Merged
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
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ npx openapi-typescript schema.yaml
| `--default-non-nullable` | | `false` | (optional) Treat schema objects with default values as non-nullable |
| `--prettier-config [location]` | `-c` | | (optional) Path to your custom Prettier configuration for output |
| `--export-type` | | `false` | (optional) Export `type` instead of `interface` |
| `--support-array-length` | | `false` | (optional) Generate tuples using array minItems / maxItems |
| `--raw-schema` | | `false` | Generate TS types from partial schema (e.g. having `components.schema` at the top level) |
| `--version` | | | Force OpenAPI version with `--version 3` or `--version 2` (required for `--raw-schema` when version is unknown) |

Expand Down
4 changes: 3 additions & 1 deletion bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Options
--prettier-config, -c (optional) specify path to Prettier config file
--raw-schema (optional) Parse as partial schema (raw components)
--export-type (optional) Export type instead of interface
--support-array-length (optional) Generate tuples using array minItems / maxItems
--version (optional) Force schema parsing version
`;

Expand All @@ -42,7 +43,7 @@ function errorAndExit(errorMessage) {
const [, , input, ...args] = process.argv;
const flags = parser(args, {
array: ["header"],
boolean: ["defaultNonNullable", "immutableTypes", "rawSchema", "exportType"],
boolean: ["defaultNonNullable", "immutableTypes", "rawSchema", "exportType", "supportArrayLength"],
number: ["version"],
string: ["auth", "header", "headersObject", "httpMethod", "prettierConfig"],
alias: {
Expand Down Expand Up @@ -92,6 +93,7 @@ async function generateSchema(pathToSpec) {
httpHeaders,
httpMethod: flags.httpMethod,
exportType: flags.exportType,
supportArrayLength: flags.supportArrayLength,
});

// output
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ async function openapiTS(
immutableTypes: options.immutableTypes || false,
rawSchema: options.rawSchema || false,
version: options.version || 3,
supportArrayLength: options.supportArrayLength,
} as any;

// note: we may be loading many large schemas into memory at once; take care to reuse references without cloning
Expand Down
26 changes: 25 additions & 1 deletion src/transform/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,31 @@ export function transformSchemaObj(node: any, options: TransformSchemaObjOptions
if (Array.isArray(node.items)) {
output += `${readonly}${tsTupleOf(node.items.map((node: any) => transformSchemaObj(node, options)))}`;
} else {
output += `${readonly}${tsArrayOf(node.items ? transformSchemaObj(node.items as any, options) : "unknown")}`;
const minItems: number = Number.isInteger(node.minItems) && node.minItems >= 0 ? node.minItems : 0;
const maxItems: number | undefined =
Number.isInteger(node.maxItems) && node.maxItems >= 0 && minItems <= node.maxItems
? node.maxItems
: undefined;

const estimateCodeSize =
maxItems === undefined ? minItems : (maxItems * (maxItems + 1) - minItems * (minItems - 1)) / 2;
const items = node.items ? transformSchemaObj(node.items as any, options) : "unknown";
if ((minItems !== 0 || maxItems !== undefined) && options.supportArrayLength && estimateCodeSize < 30) {
if (maxItems === undefined) {
output += `${readonly}${tsTupleOf([
...Array.from({ length: minItems }).map(() => items),
`...${tsArrayOf(items)}`,
])}`;
} else {
output += tsUnionOf(
Array.from({ length: maxItems - minItems + 1 })
.map((_, i) => i + minItems)
.map((n) => `${readonly}${tsTupleOf(Array.from({ length: n }).map(() => items))}`)
);
}
} else {
output += `${readonly}${tsArrayOf(items)}`;
}
}
break;
}
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,10 @@ export interface SwaggerToTSOptions {
* (optional) Export type instead of interface
*/
exportType?: boolean;
/**
* (optional) Generate tuples using array minItems / maxItems
*/
supportArrayLength?: boolean;
}

/** Context passed to all submodules */
Expand All @@ -169,4 +173,5 @@ export interface GlobalContext {
namespace?: string;
rawSchema: boolean;
version: number;
supportArrayLength?: boolean;
}
29 changes: 29 additions & 0 deletions test/core/schema.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ const defaults = {
required: new Set(),
rawSchema: false,
version: 3,
supportArrayLength: false,
};

describe("SchemaObject", () => {
Expand Down Expand Up @@ -150,6 +151,34 @@ describe("SchemaObject", () => {
);
});

it("array (supportArrayLength)", () => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome tests!

// (same as above test, but with supportArrayLength: true)
const opts = { ...defaults, supportArrayLength: true };
expect(transform({ type: "array", items: { type: "string" } }, opts)).to.equal(`(string)[]`);
expect(transform({ type: "array", items: { type: "string" }, minItems: 1 }, opts)).to.equal(
`[string, ...(string)[]]`
);
expect(transform({ type: "array", items: { type: "string" }, maxItems: 2 }, opts)).to.equal(
`([]) | ([string]) | ([string, string])`
);
expect(transform({ type: "array", items: { type: "string" }, maxItems: 20 }, opts)).to.equal(`(string)[]`);
});

it("array (immutableTypes, supportArrayLength)", () => {
// (same as above test, but with immutableTypes: true, supportArrayLength: true)
const opts = { ...defaults, immutableTypes: true, supportArrayLength: true };
expect(transform({ type: "array", items: { type: "string" } }, opts)).to.equal(`readonly (string)[]`);
expect(transform({ type: "array", items: { type: "string" }, minItems: 1 }, opts)).to.equal(
`readonly [string, ...(string)[]]`
);
expect(transform({ type: "array", items: { type: "string" }, maxItems: 2 }, opts)).to.equal(
`(readonly []) | (readonly [string]) | (readonly [string, string])`
);
expect(transform({ type: "array", items: { type: "string" }, maxItems: 20 }, opts)).to.equal(
`readonly (string)[]`
);
});

it("enum", () => {
const enumBasic = ["Totoro", "Sats'uki", "Mei"]; // note: also tests quotes in enum
expect(
Expand Down
35 changes: 35 additions & 0 deletions test/v3/expected/consts-enums.support-array-length.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* This file was auto-generated by openapi-typescript.
* Do not make direct changes to the file.
*/

export interface paths {
"/test": {
get: {
responses: {
/** A list of types. */
200: unknown;
};
};
};
}

export interface components {
schemas: {
/** @description Enum with null and nullable */
MyType: {
/** @enum {string|null} */
myEnumTestFieldNullable?: ("foo" | "bar" | null) | null;
/** @enum {string|null} */
myEnumTestField?: ("foo" | "bar" | null) | null;
/** @constant */
myConstTestField?: "constant-value";
/** @constant */
myConstTestFieldNullable?: 4 | null;
};
};
}

export interface operations {}

export interface external {}
Loading