Skip to content

Commit 5c32521

Browse files
authored
feat: generate immutable types (#522)
* feat: generate immutable types * fix: remove optional chaining * fix: use kebab-case flag * test: add immutable types tests * test: extend empty definitions with immutable types * test: extend parameters with immutable types * test: extend raw schema with immutable types * test: extend operation with immutable types * test: extend schema with immutable types * test: extend paths with immutable types * fix: immutable additional properties * test: add schema v2 immutable types tests * add npm prepare script * refactor to use function
1 parent 58a263a commit 5c32521

35 files changed

+94456
-1548
lines changed

.prettierignore

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
examples/**/*.ts
2-
tests/*/expected/**/*.ts
3-
tests/*/generated/**/*.ts
42
*.md
53
*.yaml
64
*.yml

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ For anything more complicated, or for generating specs dynamically, you can also
110110
| :----------------------------- | :---- | :------: | :--------------------------------------------------------------- |
111111
| `--output [location]` | `-o` | (stdout) | Where should the output file be saved? |
112112
| `--auth [token]` | | | (optional) Provide an auth token to be passed along in the request (only if accessing a private schema). |
113+
| `--immutable-types` | | `false` | (optional) Generates immutable types (readonly properties and readonly array). |
113114
| `--prettier-config [location]` | | | (optional) Path to your custom Prettier configuration for output |
114115
| `--raw-schema` | | `false` | Generate TS types from partial schema (e.g. having `components.schema` at the top level) |
115116

bin/cli.js

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ Options
1515
--help display this
1616
--output, -o Specify output file (default: stdout)
1717
--auth (optional) Provide an authentication token for private URL
18+
--immutable-types (optional) Generates immutable types (readonly properties and readonly array)
1819
--prettier-config (optional) specify path to Prettier config file
1920
--raw-schema (optional) Read from raw schema instead of document
2021
--version (optional) Schema version (must be present for raw schemas)
@@ -28,6 +29,9 @@ Options
2829
auth: {
2930
type: "string",
3031
},
32+
immutableTypes: {
33+
type: "boolean",
34+
},
3135
prettierConfig: {
3236
type: "string",
3337
},
@@ -69,6 +73,7 @@ async function main() {
6973

7074
// 2. generate schema (the main part!)
7175
const result = swaggerToTS(spec, {
76+
immutableTypes: cli.flags.immutableTypes,
7277
prettierConfig: cli.flags.prettierConfig,
7378
rawSchema: cli.flags.rawSchema,
7479
version: cli.flags.version,

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"build": "rm -rf dist && tsc --build tsconfig.json && tsc --build tsconfig.cjs.json",
4747
"format": "yarn prettier -w .",
4848
"lint": "eslint --ignore-path .gitignore --ext .js,.ts src",
49+
"prepare": "npm run build",
4950
"pregenerate": "npm run build",
5051
"test": "npm run build && jest --no-cache",
5152
"test:coverage": "npm run build && jest --no-cache --coverage && codecov",

src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@ export default function swaggerToTS(
2222

2323
// 2. generate output
2424
let output = `${WARNING_MESSAGE}
25-
${transformAll(schema, { version, rawSchema: options && options.rawSchema })}
25+
${transformAll(schema, {
26+
immutableTypes: (options && options.immutableTypes) || false,
27+
rawSchema: options && options.rawSchema,
28+
version,
29+
})}
2630
`;
2731

2832
// 3. Prettify output

src/transform/headers.ts

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

5-
export function transformHeaderObjMap(headerMap: Record<string, HeaderObject>): string {
5+
export function transformHeaderObjMap(
6+
headerMap: Record<string, HeaderObject>,
7+
{ immutableTypes }: { immutableTypes: boolean }
8+
): string {
69
let output = "";
710

811
Object.entries(headerMap).forEach(([k, v]) => {
912
if (!v.schema) return;
1013

1114
if (v.description) output += comment(v.description);
15+
16+
const readonly = tsReadonly(immutableTypes);
1217
const required = v.required ? "" : "?";
13-
output += ` "${k}"${required}: ${transformSchemaObj(v.schema)}\n`;
18+
19+
output += ` ${readonly}"${k}"${required}: ${transformSchemaObj(v.schema, {
20+
immutableTypes,
21+
})}\n`;
1422
});
1523

1624
return output;

src/transform/index.ts

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,20 @@
11
import { OperationObject } from "../types";
2-
import { comment } from "../utils";
2+
import { comment, tsReadonly } from "../utils";
33
import { transformHeaderObjMap } from "./headers";
44
import { transformOperationObj } from "./operation";
55
import { transformPathsObj } from "./paths";
66
import { transformResponsesObj, transformRequestBodies } from "./responses";
77
import { transformSchemaObjMap } from "./schema";
88

99
interface TransformOptions {
10+
immutableTypes: boolean;
1011
rawSchema?: boolean;
1112
version: number;
1213
}
1314

14-
export function transformAll(schema: any, { version, rawSchema }: TransformOptions): string {
15+
export function transformAll(schema: any, { immutableTypes, rawSchema, version }: TransformOptions): string {
16+
const readonly = tsReadonly(immutableTypes);
17+
1518
let output = "";
1619

1720
let operations: Record<string, OperationObject> = {};
@@ -21,11 +24,13 @@ export function transformAll(schema: any, { version, rawSchema }: TransformOptio
2124
switch (version) {
2225
case 2: {
2326
return `export interface definitions {\n ${transformSchemaObjMap(schema, {
27+
immutableTypes,
2428
required: Object.keys(schema),
2529
})}\n}`;
2630
}
2731
case 3: {
2832
return `export interface schemas {\n ${transformSchemaObjMap(schema, {
33+
immutableTypes,
2934
required: Object.keys(schema),
3035
})}\n }\n\n`;
3136
}
@@ -36,8 +41,9 @@ export function transformAll(schema: any, { version, rawSchema }: TransformOptio
3641
output += `export interface paths {\n`; // open paths
3742
if (schema.paths) {
3843
output += transformPathsObj(schema.paths, {
39-
operations,
4044
globalParameters: (schema.components && schema.components.parameters) || schema.parameters,
45+
immutableTypes,
46+
operations,
4147
version,
4248
});
4349
}
@@ -48,6 +54,7 @@ export function transformAll(schema: any, { version, rawSchema }: TransformOptio
4854
// #/definitions
4955
if (schema.definitions) {
5056
output += `export interface definitions {\n ${transformSchemaObjMap(schema.definitions, {
57+
immutableTypes,
5158
required: Object.keys(schema.definitions),
5259
})}\n}\n\n`;
5360
}
@@ -56,13 +63,16 @@ export function transformAll(schema: any, { version, rawSchema }: TransformOptio
5663
if (schema.parameters) {
5764
const required = Object.keys(schema.parameters);
5865
output += `export interface parameters {\n ${transformSchemaObjMap(schema.parameters, {
66+
immutableTypes,
5967
required,
6068
})}\n }\n\n`;
6169
}
6270

6371
// #/parameters
6472
if (schema.responses) {
65-
output += `export interface responses {\n ${transformResponsesObj(schema.responses)}\n }\n\n`;
73+
output += `export interface responses {\n ${transformResponsesObj(schema.responses, {
74+
immutableTypes,
75+
})}\n }\n\n`;
6676
}
6777
break;
6878
}
@@ -74,30 +84,40 @@ export function transformAll(schema: any, { version, rawSchema }: TransformOptio
7484
// #/components/schemas
7585
if (schema.components.schemas) {
7686
const required = Object.keys(schema.components.schemas);
77-
output += ` schemas: {\n ${transformSchemaObjMap(schema.components.schemas, { required })}\n }\n`;
87+
output += ` ${readonly}schemas: {\n ${transformSchemaObjMap(schema.components.schemas, {
88+
immutableTypes,
89+
required,
90+
})}\n }\n`;
7891
}
7992

8093
// #/components/responses
8194
if (schema.components.responses) {
82-
output += ` responses: {\n ${transformResponsesObj(schema.components.responses)}\n }\n`;
95+
output += ` ${readonly}responses: {\n ${transformResponsesObj(schema.components.responses, {
96+
immutableTypes,
97+
})}\n }\n`;
8398
}
8499

85100
// #/components/parameters
86101
if (schema.components.parameters) {
87102
const required = Object.keys(schema.components.parameters);
88-
output += ` parameters: {\n ${transformSchemaObjMap(schema.components.parameters, {
103+
output += ` ${readonly}parameters: {\n ${transformSchemaObjMap(schema.components.parameters, {
104+
immutableTypes,
89105
required,
90106
})}\n }\n`;
91107
}
92108

93109
// #/components/requestBodies
94110
if (schema.components.requestBodies) {
95-
output += ` requestBodies: {\n ${transformRequestBodies(schema.components.requestBodies)}\n }\n`;
111+
output += ` ${readonly}requestBodies: {\n ${transformRequestBodies(schema.components.requestBodies, {
112+
immutableTypes,
113+
})}\n }\n`;
96114
}
97115

98116
// #/components/headers
99117
if (schema.components.headers) {
100-
output += ` headers: {\n ${transformHeaderObjMap(schema.components.headers)} }\n`;
118+
output += ` ${readonly}headers: {\n ${transformHeaderObjMap(schema.components.headers, {
119+
immutableTypes,
120+
})} }\n`;
101121
}
102122
}
103123

@@ -110,9 +130,10 @@ export function transformAll(schema: any, { version, rawSchema }: TransformOptio
110130
if (Object.keys(operations).length) {
111131
Object.entries(operations).forEach(([operationId, operation]) => {
112132
if (operation.description) output += comment(operation.description); // handle comment
113-
output += ` "${operationId}": {\n ${transformOperationObj(operation, {
114-
version,
133+
output += ` ${readonly}"${operationId}": {\n ${transformOperationObj(operation, {
115134
globalParameters: (schema.components && schema.components.parameters) || schema.parameters,
135+
immutableTypes,
136+
version,
116137
})}\n }\n`;
117138
});
118139
}

src/transform/operation.ts

Lines changed: 24 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,57 +1,69 @@
11
import { OperationObject, ParameterObject, RequestBody } from "../types";
2-
import { comment, isRef, transformRef } from "../utils";
2+
import { comment, isRef, transformRef, tsReadonly } from "../utils";
33
import { transformParametersArray } from "./parameters";
44
import { transformResponsesObj } from "./responses";
55
import { transformSchemaObj } from "./schema";
66

77
export function transformOperationObj(
88
operation: OperationObject,
99
{
10-
version,
1110
globalParameters,
11+
immutableTypes,
12+
version,
1213
}: {
13-
version: number;
1414
globalParameters?: Record<string, ParameterObject>;
15+
immutableTypes: boolean;
16+
version: number;
1517
}
1618
): string {
19+
const readonly = tsReadonly(immutableTypes);
20+
1721
let output = "";
1822

1923
if (operation.parameters) {
20-
output += ` parameters: {\n ${transformParametersArray(operation.parameters, {
21-
version,
24+
output += ` ${readonly}parameters: {\n ${transformParametersArray(operation.parameters, {
2225
globalParameters,
26+
immutableTypes,
27+
version,
2328
})}\n }\n`;
2429
}
2530

2631
if (operation.responses) {
27-
output += ` responses: {\n ${transformResponsesObj(operation.responses)}\n }\n`;
32+
output += ` ${readonly}responses: {\n ${transformResponsesObj(operation.responses, {
33+
immutableTypes,
34+
})}\n }\n`;
2835
}
2936

3037
if (operation.requestBody) {
3138
if (isRef(operation.requestBody)) {
32-
output += ` requestBody: ${transformRef(operation.requestBody.$ref)};\n`;
39+
output += ` ${readonly}requestBody: ${transformRef(operation.requestBody.$ref)};\n`;
3340
} else {
3441
if (operation.requestBody.description) output += comment(operation.requestBody.description);
3542

36-
output += ` requestBody: {\n`; // open requestBody
37-
output += ` ${transformRequestBodyObj(operation.requestBody)}`;
43+
output += ` ${readonly}requestBody: {\n`; // open requestBody
44+
output += ` ${transformRequestBodyObj(operation.requestBody, { immutableTypes })}`;
3845
output += ` }\n`; // close requestBody
3946
}
4047
}
4148

4249
return output;
4350
}
4451

45-
export function transformRequestBodyObj(requestBody: RequestBody) {
52+
export function transformRequestBodyObj(
53+
requestBody: RequestBody,
54+
{ immutableTypes }: { immutableTypes: boolean }
55+
): string {
56+
const readonly = tsReadonly(immutableTypes);
57+
4658
let output = "";
4759

4860
const { content } = requestBody;
4961

5062
if (content && Object.keys(content).length) {
51-
output += ` content: {\n`; // open content
63+
output += ` ${readonly}content: {\n`; // open content
5264

5365
Object.entries(content).forEach(([k, v]) => {
54-
output += ` "${k}": ${transformSchemaObj(v.schema)};\n`;
66+
output += ` ${readonly}"${k}": ${transformSchemaObj(v.schema, { immutableTypes })};\n`;
5567
});
5668
output += ` }\n`; // close content
5769
} else {

src/transform/parameters.ts

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

55
export function transformParametersArray(
66
parameters: (ReferenceObject | ParameterObject)[],
77
{
8-
version,
98
globalParameters,
9+
immutableTypes,
10+
version,
1011
}: {
11-
version: number;
1212
globalParameters?: Record<string, ParameterObject>;
13+
immutableTypes: boolean;
14+
version: number;
1315
}
1416
): string {
17+
const readonly = tsReadonly(immutableTypes);
18+
1519
let output = "";
1620

1721
// sort into map
@@ -44,7 +48,7 @@ export function transformParametersArray(
4448

4549
// transform output
4650
Object.entries(mappedParams).forEach(([paramIn, paramGroup]) => {
47-
output += ` ${paramIn}: {\n`; // open in
51+
output += ` ${readonly}${paramIn}: {\n`; // open in
4852
Object.entries(paramGroup).forEach(([paramName, paramObj]) => {
4953
let paramComment = "";
5054
if (paramObj.deprecated) paramComment += `@deprecated `;
@@ -55,16 +59,16 @@ export function transformParametersArray(
5559
let paramType = ``;
5660
if (version === 2) {
5761
if (paramObj.in === "body" && paramObj.schema) {
58-
paramType = transformSchemaObj(paramObj.schema);
62+
paramType = transformSchemaObj(paramObj.schema, { immutableTypes });
5963
} else if (paramObj.type) {
60-
paramType = transformSchemaObj(paramObj);
64+
paramType = transformSchemaObj(paramObj, { immutableTypes });
6165
} else {
6266
paramType = "unknown";
6367
}
6468
} else if (version === 3) {
65-
paramType = paramObj.schema ? transformSchemaObj(paramObj.schema) : "unknown";
69+
paramType = paramObj.schema ? transformSchemaObj(paramObj.schema, { immutableTypes }) : "unknown";
6670
}
67-
output += ` "${paramName}"${required}: ${paramType};\n`;
71+
output += ` ${readonly}"${paramName}"${required}: ${paramType};\n`;
6872
});
6973
output += ` }\n`; // close in
7074
});

0 commit comments

Comments
 (0)