Skip to content

Commit 16c25c6

Browse files
drwpowMrLeebo
andauthored
Alphabetically sort transforms (#966)
* Alphabetically sort output from transforms * Refactor sort into an --alphabetize flag and add test cases * Fix typo * Rewrite tests to test for alphabetization of specific object types * Add test for alphabetized operations and clean up operation tests * fix TS error Co-authored-by: Jeremy Liberman <mrleebo@msn.com>
1 parent d32cadd commit 16c25c6

17 files changed

+536
-27
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ npx openapi-typescript schema.yaml
155155
| `--support-array-length` | | `false` | (optional) Generate tuples using array minItems / maxItems |
156156
| `--make-paths-enum` | `-pe` | `false` | (optional) Generate an enum of endpoint paths |
157157
| `--path-params-as-types` | | `false` | (optional) Substitute path parameter names with their respective types |
158+
| `--alphabetize` | | `false` | (optional) Sort types alphabetically |
158159
| `--raw-schema` | | `false` | Generate TS types from partial schema (e.g. having `components.schema` at the top level) |
159160

160161
### 🐢 Node

bin/cli.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ Options
3030
--export-type (optional) Export type instead of interface
3131
--support-array-length (optional) Generate tuples using array minItems / maxItems
3232
--path-params-as-types (optional) Substitute path parameter names with their respective types
33+
--alphabetize (optional) Sort types alphabetically
3334
--version (optional) Force schema parsing version
3435
`;
3536

@@ -57,6 +58,7 @@ const flags = parser(args, {
5758
"supportArrayLength",
5859
"makePathsEnum",
5960
"pathParamsAsTypes",
61+
"alphabetize",
6062
],
6163
number: ["version"],
6264
string: ["auth", "header", "headersObject", "httpMethod", "prettierConfig"],
@@ -112,6 +114,7 @@ async function generateSchema(pathToSpec) {
112114
exportType: flags.exportType,
113115
supportArrayLength: flags.supportArrayLength,
114116
pathParamsAsTypes: flags.pathParamsAsTypes,
117+
alphabetize: flags.alphabetize,
115118
});
116119

117120
// output

src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ async function openapiTS(
4646
contentNever: options.contentNever || false,
4747
makePathsEnum: options.makePathsEnum || false,
4848
pathParamsAsTypes: options.pathParamsAsTypes,
49+
alphabetize: options.alphabetize || false,
4950
rawSchema: options.rawSchema || false,
5051
supportArrayLength: options.supportArrayLength,
5152
version: options.version || 3,

src/transform/headers.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { GlobalContext, HeaderObject } from "../types.js";
2-
import { comment, tsReadonly } from "../utils.js";
2+
import { comment, getEntries, tsReadonly } from "../utils.js";
33
import { transformSchemaObj } from "./schema.js";
44

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

15-
for (const k of Object.keys(headerMap)) {
16-
const v = headerMap[k];
15+
for (const [k, v] of getEntries(headerMap, options)) {
1716
if (!v.schema) continue;
1817

1918
if (v.description) output += comment(v.description);

src/transform/parameters.ts

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { GlobalContext, ParameterObject, ReferenceObject } from "../types.js";
2-
import { comment, tsReadonly } from "../utils.js";
2+
import { comment, getEntries, tsReadonly } from "../utils.js";
33
import { transformSchemaObj } from "./schema.js";
44

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

2020
// sort into map
2121
const mappedParams: Record<string, Record<string, ParameterObject>> = {};
22-
for (const paramObj of parameters as any[]) {
23-
if (paramObj.$ref && globalParameters) {
24-
const paramName = paramObj.$ref.split('["').pop().replace(PARAM_END_RE, ""); // take last segment
22+
for (const paramObj of parameters) {
23+
if ("$ref" in paramObj && paramObj.$ref && globalParameters) {
24+
// take last segment
25+
let paramName = paramObj.$ref.split('["').pop();
26+
paramName = String(paramName).replace(PARAM_END_RE, "");
27+
2528
if (globalParameters[paramName]) {
26-
const reference = globalParameters[paramName] as any;
29+
const reference = globalParameters[paramName];
30+
if (!reference.in) continue;
31+
2732
if (!mappedParams[reference.in]) mappedParams[reference.in] = {};
2833
switch (ctx.version) {
2934
case 3: {
@@ -36,7 +41,7 @@ export function transformParametersArray(
3641
case 2: {
3742
mappedParams[reference.in][reference.name || paramName] = {
3843
...reference,
39-
$ref: paramObj.$ref,
44+
...("$ref" in paramObj ? { $ref: paramObj.$ref } : null),
4045
};
4146
break;
4247
}
@@ -45,15 +50,16 @@ export function transformParametersArray(
4550
continue;
4651
}
4752

53+
if (!("in" in paramObj)) continue;
4854
if (!paramObj.in || !paramObj.name) continue;
4955
if (!mappedParams[paramObj.in]) mappedParams[paramObj.in] = {};
5056
mappedParams[paramObj.in][paramObj.name] = paramObj;
5157
}
5258

5359
// transform output
54-
for (const [paramIn, paramGroup] of Object.entries(mappedParams)) {
60+
for (const [paramIn, paramGroup] of getEntries(mappedParams, ctx)) {
5561
output += ` ${readonly}${paramIn}: {\n`; // open in
56-
for (const [paramName, paramObj] of Object.entries(paramGroup)) {
62+
for (const [paramName, paramObj] of getEntries(paramGroup, ctx)) {
5763
let paramComment = "";
5864
if (paramObj.deprecated) paramComment += `@deprecated `;
5965
if (paramObj.description) paramComment += paramObj.description;

src/transform/paths.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { GlobalContext, OperationObject, ParameterObject, PathItemObject } from "../types.js";
2-
import { comment, tsReadonly, nodeType } from "../utils.js";
2+
import { comment, tsReadonly, nodeType, getEntries } from "../utils.js";
33
import { transformOperationObj } from "./operation.js";
44
import { transformParametersArray } from "./parameters.js";
55

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

3434
let output = "";
3535

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

3939
if (pathItem.$ref) {

src/transform/request.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
import type { GlobalContext, RequestBody } from "../types.js";
2-
import { comment, tsReadonly } from "../utils.js";
2+
import { comment, getEntries, tsReadonly } from "../utils.js";
33
import { transformSchemaObj } from "./schema.js";
44

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

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

2121
if (requestBody.content && Object.keys(requestBody.content).length) {
2222
output += ` ${readonly}content: {\n`; // open content
23-
for (const [k, v] of Object.entries(requestBody.content)) {
23+
for (const [k, v] of getEntries(requestBody.content, ctx)) {
2424
output += ` ${readonly}"${k}": ${transformSchemaObj(v.schema, { ...ctx, required: new Set<string>() })};\n`;
2525
}
2626
output += ` }\n`; // close content

src/transform/responses.ts

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type { GlobalContext } from "../types.js";
2-
import { comment, tsReadonly } from "../utils.js";
2+
import { comment, getEntries, tsReadonly } from "../utils.js";
33
import { transformHeaderObjMap } from "./headers.js";
44
import { transformSchemaObj } from "./schema.js";
55

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

1616
let output = "";
1717

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

2322
if (response.$ref) {
@@ -48,10 +47,10 @@ export function transformResponsesObj(responsesObj: Record<string, any>, ctx: Gl
4847
switch (ctx.version) {
4948
case 3: {
5049
output += ` ${readonly}content: {\n`; // open content
51-
for (const contentType of Object.keys(response.content)) {
52-
const contentResponse = response.content[contentType] as any;
50+
// TODO: proper type definitions for this
51+
for (const [contentType, contentResponse] of getEntries<any>(response.content, ctx)) {
5352
const responseType =
54-
contentResponse && contentResponse?.schema
53+
"schema" in contentResponse
5554
? transformSchemaObj(contentResponse.schema, { ...ctx, required: new Set<string>() })
5655
: "unknown";
5756
output += ` ${readonly}"${contentType}": ${responseType};\n`;

src/transform/schema.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
tsUnionOf,
1010
parseSingleSimpleValue,
1111
ParsedSimpleValue,
12+
getEntries,
1213
} from "../utils.js";
1314

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

30-
for (const k of Object.keys(obj)) {
31-
const v = obj[k];
32-
31+
for (const [k, v] of getEntries(obj, options)) {
3332
// 1. Add comment in jsdoc notation
3433
const comment = prepareComment(v);
3534
if (comment) output += comment;

src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ export interface SwaggerToTSOptions {
136136
rawSchema?: boolean;
137137
/** (optional) Generate an enum containing all API paths. **/
138138
makePathsEnum?: boolean;
139+
/** (optional) Sort types alphabetically. */
140+
alphabetize?: boolean;
139141
/** (optional) Should logging be suppressed? (necessary for STDOUT) */
140142
silent?: boolean;
141143
/** (optional) OpenAPI version. Must be present if parsing raw schema */
@@ -186,6 +188,7 @@ export interface GlobalContext {
186188
makePathsEnum: boolean;
187189
namespace?: string;
188190
pathParamsAsTypes?: boolean;
191+
alphabetize?: boolean;
189192
rawSchema: boolean;
190193
silent?: boolean;
191194
supportArrayLength?: boolean;

src/utils.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import type { OpenAPI2, OpenAPI3, ReferenceObject } from "./types.js";
1+
import type { GlobalContext, OpenAPI2, OpenAPI3, ReferenceObject } from "./types.js";
22

33
type CommentObject = {
44
const?: boolean; // jsdoc without value
@@ -292,3 +292,9 @@ export function replaceKeys(obj: Record<string, any>): Record<string, any> {
292292
return obj;
293293
}
294294
}
295+
296+
export function getEntries<Item>(obj: ArrayLike<Item> | Record<string, Item>, options: GlobalContext) {
297+
const entries = Object.entries(obj);
298+
if (options.alphabetize) entries.sort(([a], [b]) => a.localeCompare(b, "en", { numeric: true }));
299+
return entries;
300+
}

test/core/operation.test.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,4 +187,108 @@ describe("parameters", () => {
187187
188188
}`);
189189
});
190+
191+
describe("alphabetize", () => {
192+
function assertSchema(actual, expected) {
193+
const result = transformOperationObj(actual, {
194+
...defaults,
195+
alphabetize: true,
196+
version: 3,
197+
pathItem: {
198+
parameters: [
199+
{
200+
in: "path",
201+
name: "p2",
202+
schema: {
203+
type: "string",
204+
},
205+
},
206+
{
207+
in: "path",
208+
name: "p3",
209+
schema: {
210+
type: "string",
211+
},
212+
},
213+
],
214+
},
215+
});
216+
expect(result.trim()).to.equal(expected.trim());
217+
}
218+
219+
it("content types", () => {
220+
const actual = {
221+
requestBody: {
222+
content: {
223+
"font/woff2": {
224+
schema: { type: "string" },
225+
},
226+
"font/otf": {
227+
schema: { type: "string" },
228+
},
229+
"font/sfnt": {
230+
schema: { type: "string" },
231+
},
232+
"font/ttf": {
233+
schema: { type: "string" },
234+
},
235+
"font/woff": {
236+
schema: { type: "string" },
237+
},
238+
},
239+
},
240+
};
241+
242+
const expected = `parameters: {
243+
path: {
244+
"p2"?: string;
245+
"p3"?: string;
246+
}
247+
248+
}
249+
requestBody: {
250+
content: {
251+
"font/otf": string;
252+
"font/sfnt": string;
253+
"font/ttf": string;
254+
"font/woff": string;
255+
"font/woff2": string;
256+
}
257+
}`;
258+
259+
assertSchema(actual, expected);
260+
});
261+
262+
it("operation parameters", () => {
263+
const actual = {
264+
parameters: [
265+
{
266+
in: "path",
267+
name: "p2",
268+
schema: {
269+
type: "number",
270+
},
271+
},
272+
{
273+
in: "path",
274+
name: "p1",
275+
schema: {
276+
type: "string",
277+
},
278+
},
279+
],
280+
};
281+
282+
const expected = `parameters: {
283+
path: {
284+
"p1"?: string;
285+
"p2"?: number;
286+
"p3"?: string;
287+
}
288+
289+
}`;
290+
291+
assertSchema(actual, expected);
292+
});
293+
});
190294
});

0 commit comments

Comments
 (0)