Skip to content

New --alphabetize switch: Sorts output file consistently #942

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

Closed
Closed
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
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ If you’ve added a feature or fixed a bug and need to update the generated sche
# 1. re-build the package
npm run build
# 2. run the local CLI (not the npm one!)
./bin/cli.js tests/v3/specs/github.yaml -o tests/v3/expected/github.ts
./bin/cli.js test/v3/specs/github.yaml -o test/v3/expected/github.ts
# NOTE: on Windows, try running the script on WSL if getting errors
```

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ npx openapi-typescript schema.yaml
| `--support-array-length` | | `false` | (optional) Generate tuples using array minItems / maxItems |
| `--make-paths-enum` | `-pe` | `false` | (optional) Generate an enum of endpoint paths |
| `--path-params-as-types` | | `false` | (optional) Substitute path parameter names with their respective types |
| `--alphabetize` | | `false` | (optional) Sort types alphabetically |
| `--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
3 changes: 3 additions & 0 deletions bin/cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Options
--export-type (optional) Export type instead of interface
--support-array-length (optional) Generate tuples using array minItems / maxItems
--path-params-as-types (optional) Substitute path parameter names with their respective types
--alphabetize (optional) Sort types alphabetically
--version (optional) Force schema parsing version
`;

Expand All @@ -55,6 +56,7 @@ const flags = parser(args, {
"supportArrayLength",
"makePathsEnum",
"pathParamsAsTypes",
"alphabetize",
],
number: ["version"],
string: ["auth", "header", "headersObject", "httpMethod", "prettierConfig"],
Expand Down Expand Up @@ -110,6 +112,7 @@ async function generateSchema(pathToSpec) {
exportType: flags.exportType,
supportArrayLength: flags.supportArrayLength,
pathParamsAsTypes: flags.pathParamsAsTypes,
alphabetize: flags.alphabetize,
});

// output
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ async function openapiTS(
contentNever: options.contentNever || false,
makePathsEnum: options.makePathsEnum || false,
pathParamsAsTypes: options.pathParamsAsTypes,
alphabetize: options.alphabetize || false,
rawSchema: options.rawSchema || false,
supportArrayLength: options.supportArrayLength,
version: options.version || 3,
Expand Down
5 changes: 2 additions & 3 deletions src/transform/headers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { GlobalContext, HeaderObject } from "../types.js";
import { comment, tsReadonly } from "../utils.js";
import { comment, getEntries, tsReadonly } from "../utils.js";
import { transformSchemaObj } from "./schema.js";

interface TransformHeadersOptions extends GlobalContext {
Expand All @@ -12,8 +12,7 @@ export function transformHeaderObjMap(
): string {
let output = "";

for (const k of Object.keys(headerMap)) {
const v = headerMap[k];
for (const [k, v] of getEntries(headerMap, options)) {
if (!v.schema) continue;

if (v.description) output += comment(v.description);
Expand Down
22 changes: 14 additions & 8 deletions src/transform/parameters.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { GlobalContext, ParameterObject, ReferenceObject } from "../types.js";
import { comment, tsReadonly } from "../utils.js";
import { comment, getEntries, tsReadonly } from "../utils.js";
import { transformSchemaObj } from "./schema.js";

interface TransformParametersOptions extends GlobalContext {
Expand All @@ -19,11 +19,16 @@ export function transformParametersArray(

// sort into map
const mappedParams: Record<string, Record<string, ParameterObject>> = {};
for (const paramObj of parameters as any[]) {
if (paramObj.$ref && globalParameters) {
const paramName = paramObj.$ref.split('["').pop().replace(PARAM_END_RE, ""); // take last segment
for (const paramObj of parameters) {
if ("$ref" in paramObj && paramObj.$ref && globalParameters) {
// take last segment
let paramName = paramObj.$ref.split('["').pop();
paramName = String(paramName).replace(PARAM_END_RE, "");

if (globalParameters[paramName]) {
const reference = globalParameters[paramName] as any;
const reference = globalParameters[paramName];
if (!reference.in) continue;

if (!mappedParams[reference.in]) mappedParams[reference.in] = {};
switch (ctx.version) {
case 3: {
Expand All @@ -36,7 +41,7 @@ export function transformParametersArray(
case 2: {
mappedParams[reference.in][reference.name || paramName] = {
...reference,
$ref: paramObj.$ref,
...("$ref" in paramObj ? { $ref: paramObj.$ref } : null),
};
break;
}
Expand All @@ -45,15 +50,16 @@ export function transformParametersArray(
continue;
}

if (!("in" in paramObj)) continue;
if (!paramObj.in || !paramObj.name) continue;
if (!mappedParams[paramObj.in]) mappedParams[paramObj.in] = {};
mappedParams[paramObj.in][paramObj.name] = paramObj;
}

// transform output
for (const [paramIn, paramGroup] of Object.entries(mappedParams)) {
for (const [paramIn, paramGroup] of getEntries(mappedParams, ctx)) {
output += ` ${readonly}${paramIn}: {\n`; // open in
for (const [paramName, paramObj] of Object.entries(paramGroup)) {
for (const [paramName, paramObj] of getEntries(paramGroup, ctx)) {
let paramComment = "";
if (paramObj.deprecated) paramComment += `@deprecated `;
if (paramObj.description) paramComment += paramObj.description;
Expand Down
4 changes: 2 additions & 2 deletions src/transform/paths.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { GlobalContext, OperationObject, ParameterObject, PathItemObject } from "../types.js";
import { comment, tsReadonly, nodeType } from "../utils.js";
import { comment, tsReadonly, nodeType, getEntries } from "../utils.js";
import { transformOperationObj } from "./operation.js";
import { transformParametersArray } from "./parameters.js";

Expand Down Expand Up @@ -33,7 +33,7 @@ export function transformPathsObj(paths: Record<string, PathItemObject>, options

let output = "";

for (const [url, pathItem] of Object.entries(paths)) {
for (const [url, pathItem] of getEntries(paths, options)) {
if (pathItem.description) output += comment(pathItem.description); // add comment

if (pathItem.$ref) {
Expand Down
6 changes: 3 additions & 3 deletions src/transform/request.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
import type { GlobalContext, RequestBody } from "../types.js";
import { comment, tsReadonly } from "../utils.js";
import { comment, getEntries, tsReadonly } from "../utils.js";
import { transformSchemaObj } from "./schema.js";

export function transformRequestBodies(requestBodies: Record<string, RequestBody>, ctx: GlobalContext) {
let output = "";

for (const [name, requestBody] of Object.entries(requestBodies)) {
for (const [name, requestBody] of getEntries(requestBodies, ctx)) {
if (requestBody && requestBody.description) output += ` ${comment(requestBody.description)}`;
output += ` "${name}": {\n ${transformRequestBodyObj(requestBody, ctx)}\n }\n`;
}
Expand All @@ -20,7 +20,7 @@ export function transformRequestBodyObj(requestBody: RequestBody, ctx: GlobalCon

if (requestBody.content && Object.keys(requestBody.content).length) {
output += ` ${readonly}content: {\n`; // open content
for (const [k, v] of Object.entries(requestBody.content)) {
for (const [k, v] of getEntries(requestBody.content, ctx)) {
output += ` ${readonly}"${k}": ${transformSchemaObj(v.schema, { ...ctx, required: new Set<string>() })};\n`;
}
output += ` }\n`; // close content
Expand Down
11 changes: 5 additions & 6 deletions src/transform/responses.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { GlobalContext } from "../types.js";
import { comment, tsReadonly } from "../utils.js";
import { comment, getEntries, tsReadonly } from "../utils.js";
import { transformHeaderObjMap } from "./headers.js";
import { transformSchemaObj } from "./schema.js";

Expand All @@ -15,9 +15,8 @@ export function transformResponsesObj(responsesObj: Record<string, any>, ctx: Gl

let output = "";

for (const httpStatusCode of Object.keys(responsesObj)) {
for (const [httpStatusCode, response] of getEntries(responsesObj, ctx)) {
const statusCode = Number(httpStatusCode) || `"${httpStatusCode}"`; // don’t surround w/ quotes if numeric status code
const response = responsesObj[httpStatusCode];
if (response.description) output += comment(response.description);

if (response.$ref) {
Expand Down Expand Up @@ -48,10 +47,10 @@ export function transformResponsesObj(responsesObj: Record<string, any>, ctx: Gl
switch (ctx.version) {
case 3: {
output += ` ${readonly}content: {\n`; // open content
for (const contentType of Object.keys(response.content)) {
const contentResponse = response.content[contentType] as any;
// TODO: proper type definitions for this
for (const [contentType, contentResponse] of getEntries<any>(response.content, ctx)) {
const responseType =
contentResponse && contentResponse?.schema
"schema" in contentResponse
? transformSchemaObj(contentResponse.schema, { ...ctx, required: new Set<string>() })
: "unknown";
output += ` ${readonly}"${contentType}": ${responseType};\n`;
Expand Down
5 changes: 2 additions & 3 deletions src/transform/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
tsUnionOf,
parseSingleSimpleValue,
ParsedSimpleValue,
getEntries,
} from "../utils.js";

interface TransformSchemaObjOptions extends GlobalContext {
Expand All @@ -27,9 +28,7 @@ function hasDefaultValue(node: any): boolean {
export function transformSchemaObjMap(obj: Record<string, any>, options: TransformSchemaObjOptions): string {
let output = "";

for (const k of Object.keys(obj)) {
const v = obj[k];

for (const [k, v] of getEntries(obj, options)) {
// 1. Add comment in jsdoc notation
const comment = prepareComment(v);
if (comment) output += comment;
Expand Down
3 changes: 3 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ export interface SwaggerToTSOptions {
rawSchema?: boolean;
/** (optional) Generate an enum containing all API paths. **/
makePathsEnum?: boolean;
/** (optional) Sort types alphabetically. */
alphabetize?: boolean;
/** (optional) Should logging be suppressed? (necessary for STDOUT) */
silent?: boolean;
/** (optional) OpenAPI version. Must be present if parsing raw schema */
Expand Down Expand Up @@ -186,6 +188,7 @@ export interface GlobalContext {
makePathsEnum: boolean;
namespace?: string;
pathParamsAsTypes?: boolean;
alphabetize?: boolean;
rawSchema: boolean;
silent?: boolean;
supportArrayLength?: boolean;
Expand Down
8 changes: 7 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { OpenAPI2, OpenAPI3, ReferenceObject } from "./types.js";
import type { GlobalContext, OpenAPI2, OpenAPI3, ReferenceObject } from "./types.js";

type CommentObject = {
const?: boolean; // jsdoc without value
Expand Down Expand Up @@ -292,3 +292,9 @@ export function replaceKeys(obj: Record<string, any>): Record<string, any> {
return obj;
}
}

export function getEntries<Item>(obj: ArrayLike<Item> | Record<string, Item>, options: GlobalContext) {
const entries = Object.entries(obj);
if (options.alphabetize) entries.sort(([a], [b]) => a.localeCompare(b, "en", { numeric: true }));
return entries;
}
104 changes: 104 additions & 0 deletions test/core/operation.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,108 @@ describe("parameters", () => {

}`);
});

describe("alphabetize", () => {
function assertSchema(actual, expected) {
const result = transformOperationObj(actual, {
...defaults,
alphabetize: true,
version: 3,
pathItem: {
parameters: [
{
in: "path",
name: "p2",
schema: {
type: "string",
},
},
{
in: "path",
name: "p3",
schema: {
type: "string",
},
},
],
},
});
expect(result.trim()).to.equal(expected.trim());
}

it("content types", () => {
const actual = {
requestBody: {
content: {
"font/woff2": {
schema: { type: "string" },
},
"font/otf": {
schema: { type: "string" },
},
"font/sfnt": {
schema: { type: "string" },
},
"font/ttf": {
schema: { type: "string" },
},
"font/woff": {
schema: { type: "string" },
},
},
},
};

const expected = `parameters: {
path: {
"p2"?: string;
"p3"?: string;
}

}
requestBody: {
content: {
"font/otf": string;
"font/sfnt": string;
"font/ttf": string;
"font/woff": string;
"font/woff2": string;
}
}`;

assertSchema(actual, expected);
});

it("operation parameters", () => {
const actual = {
parameters: [
{
in: "path",
name: "p2",
schema: {
type: "number",
},
},
{
in: "path",
name: "p1",
schema: {
type: "string",
},
},
],
};

const expected = `parameters: {
path: {
"p1"?: string;
"p2"?: number;
"p3"?: string;
}

}`;

assertSchema(actual, expected);
});
});
});
Loading