Skip to content

Commit 35fc219

Browse files
authored
feat: [generateFetchers] Add extra props support (#25)
* Add extra props to variables * Add readFile interface * Generate extraProps only if needed
1 parent 73209e5 commit 35fc219

File tree

12 files changed

+259
-96
lines changed

12 files changed

+259
-96
lines changed

cli/src/commands/GenerateCommand.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { Config, Namespace } from "../types";
1010
import { getOpenAPISourceFile } from "../core/getOpenAPISourceFile.js";
1111
import { parseOpenAPISourceFile } from "../core/parseOpenAPISourceFile.js";
1212

13-
const { unlink, outputFile, existsSync } = fsExtra;
1413
const __filename = fileURLToPath(import.meta.url);
1514

1615
// if no config -> tell the user to do `openapi-codegen init`
@@ -54,15 +53,15 @@ export class GenerateCommand extends Command {
5453
});
5554

5655
// Write the transpiled file (.js)
57-
await outputFile(transpiledPath, code);
56+
await fsExtra.outputFile(transpiledPath, code);
5857

5958
// Compute the result
6059
const { default: config } = await import(
6160
path.relative(path.parse(slash(__filename)).dir, slash(transpiledPath))
6261
);
6362

6463
// Delete the transpiled file
65-
await unlink(transpiledPath);
64+
await fsExtra.unlink(transpiledPath);
6665

6766
// Return the result
6867
return config;
@@ -88,21 +87,31 @@ export class GenerateCommand extends Command {
8887
const prettierConfig = await prettier.resolveConfig(process.cwd());
8988

9089
const writeFile = async (file: string, data: string) => {
91-
await outputFile(
90+
await fsExtra.outputFile(
9291
path.join(process.cwd(), config.outputDir, file),
9392
prettier.format(data, { parser: "babel-ts", ...prettierConfig })
9493
);
9594
};
9695

96+
const readFile = (file: string) => {
97+
return fsExtra.readFile(
98+
path.join(process.cwd(), config.outputDir, file),
99+
"utf-8"
100+
);
101+
};
102+
97103
const existsFile = (file: string) => {
98-
return existsSync(path.join(process.cwd(), config.outputDir, file));
104+
return fsExtra.existsSync(
105+
path.join(process.cwd(), config.outputDir, file)
106+
);
99107
};
100108

101109
await config.to({
102110
openAPIDocument,
103111
outputDir: config.outputDir,
104112
writeFile,
105113
existsFile,
114+
readFile,
106115
});
107116
}
108117
}

cli/src/types/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ export type Context = {
7272
openAPIDocument: OpenAPIObject;
7373
outputDir: string;
7474
writeFile: (file: string, data: string) => Promise<void>;
75+
readFile: (file: string) => Promise<string>;
7576
existsFile: (file: string) => boolean;
7677
};
7778

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import { omit } from "lodash";
2+
import ts, { factory } from "typescript";
3+
import { OperationObject } from "openapi3-ts";
4+
5+
import { petstore } from "../fixtures/petstore";
6+
import { getOperationTypes } from "./getOperationTypes";
7+
8+
describe("getOperationTypes", () => {
9+
it("should generate a variable type (with extra props)", () => {
10+
const output = getOperationTypes({
11+
operationId: "listPet",
12+
operation: petstore.paths["/pets"].get as OperationObject,
13+
openAPIDocument: petstore,
14+
printNodes: () => "",
15+
variablesExtraPropsType: factory.createTypeReferenceNode("ExtraProps"),
16+
});
17+
18+
expect(print(output.declarationNodes[2])).toMatchInlineSnapshot(`
19+
"export type ListPetVariables = {
20+
queryParams?: ListPetQueryParams;
21+
} & ExtraProps;"
22+
`);
23+
});
24+
25+
it("should generate a variable type (without extra props)", () => {
26+
const output = getOperationTypes({
27+
operationId: "listPet",
28+
operation: petstore.paths["/pets"].get as OperationObject,
29+
openAPIDocument: petstore,
30+
printNodes: () => "",
31+
variablesExtraPropsType: factory.createKeywordTypeNode(
32+
ts.SyntaxKind.VoidKeyword
33+
),
34+
});
35+
36+
expect(print(output.declarationNodes[2])).toMatchInlineSnapshot(`
37+
"export type ListPetVariables = {
38+
queryParams?: ListPetQueryParams;
39+
};"
40+
`);
41+
});
42+
43+
it("should generate a variable type (with extra props only)", () => {
44+
const output = getOperationTypes({
45+
operationId: "listPet",
46+
operation: omit(
47+
petstore.paths["/pets"].get,
48+
"parameters"
49+
) as OperationObject,
50+
openAPIDocument: petstore,
51+
printNodes: () => "",
52+
variablesExtraPropsType: factory.createTypeReferenceNode("ExtraProps"),
53+
});
54+
55+
expect(print(output.declarationNodes[1])).toMatchInlineSnapshot(
56+
`"export type ListPetVariables = ExtraProps;"`
57+
);
58+
});
59+
60+
it("should generate a variable type (void)", () => {
61+
const output = getOperationTypes({
62+
operationId: "listPet",
63+
operation: omit(
64+
petstore.paths["/pets"].get,
65+
"parameters"
66+
) as OperationObject,
67+
openAPIDocument: petstore,
68+
printNodes: () => "",
69+
variablesExtraPropsType: factory.createKeywordTypeNode(
70+
ts.SyntaxKind.VoidKeyword
71+
),
72+
});
73+
74+
expect(output.declarationNodes.length).toBe(1);
75+
});
76+
});
77+
78+
// Helpers
79+
const sourceFile = ts.createSourceFile("index.ts", "", ts.ScriptTarget.Latest);
80+
81+
const printer = ts.createPrinter({
82+
newLine: ts.NewLineKind.LineFeed,
83+
removeComments: false,
84+
});
85+
86+
const print = (node: ts.Node) =>
87+
printer.printNode(ts.EmitHint.Unspecified, node, sourceFile);

plugins/typescript/src/core/getOperationTypes.ts

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,7 @@ export type GetOperationTypesOptions = {
1717
injectedHeaders?: string[];
1818
pathParameters?: PathItemObject["parameters"];
1919
printNodes: (nodes: ts.Node[]) => string;
20-
contextTypeName: string;
21-
withContextType: boolean;
20+
variablesExtraPropsType: ts.TypeNode;
2221
};
2322

2423
export type GetOperationTypesOutput = {
@@ -40,10 +39,9 @@ export const getOperationTypes = ({
4039
operation,
4140
openAPIDocument,
4241
printNodes,
43-
withContextType,
4442
pathParameters = [],
4543
injectedHeaders = [],
46-
contextTypeName,
44+
variablesExtraPropsType,
4745
}: GetOperationTypesOptions): GetOperationTypesOutput => {
4846
const declarationNodes: ts.Node[] = [];
4947

@@ -201,15 +199,23 @@ export const getOperationTypes = ({
201199
headersType,
202200
pathParamsType,
203201
queryParamsType,
204-
contextTypeName,
205202
headersOptional,
206203
pathParamsOptional,
207204
queryParamsOptional,
208205
requestBodyOptional,
209-
withContextType,
210206
});
211207

212-
if (shouldExtractNode(variablesType) || withContextType) {
208+
if (variablesExtraPropsType.kind !== ts.SyntaxKind.VoidKeyword) {
209+
variablesType =
210+
variablesType.kind === ts.SyntaxKind.VoidKeyword
211+
? variablesExtraPropsType
212+
: f.createIntersectionTypeNode([
213+
variablesType,
214+
variablesExtraPropsType,
215+
]);
216+
}
217+
218+
if (variablesType.kind !== ts.SyntaxKind.VoidKeyword) {
213219
declarationNodes.push(
214220
f.createTypeAliasDeclaration(
215221
undefined,
@@ -219,6 +225,7 @@ export const getOperationTypes = ({
219225
variablesType
220226
)
221227
);
228+
222229
variablesType = f.createTypeReferenceNode(variablesIdentifier);
223230
}
224231

plugins/typescript/src/core/getVariablesType.test.ts

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,27 @@ import ts, { factory as f } from "typescript";
22
import { getVariablesType } from "./getVariablesType";
33

44
describe("getVariableType", () => {
5-
it("should return the fetcherOption type if no types are provided", () => {
5+
it("should return void if no types are provided", () => {
66
const variablesType = getVariablesType({
7-
withContextType: true,
87
requestBodyType: undefinedType,
98
headersType: undefinedType,
109
pathParamsType: undefinedType,
1110
queryParamsType: undefinedType,
12-
contextTypeName: "MyContext",
1311
headersOptional: false,
1412
pathParamsOptional: false,
1513
queryParamsOptional: false,
1614
requestBodyOptional: false,
1715
});
1816

19-
expect(print(variablesType)).toMatchInlineSnapshot(
20-
`"MyContext[\\"fetcherOptions\\"]"`
21-
);
17+
expect(print(variablesType)).toMatchInlineSnapshot(`"void"`);
2218
});
2319

2420
it("should have requestBody type declared", () => {
2521
const variablesType = getVariablesType({
26-
withContextType: true,
2722
requestBodyType: f.createKeywordTypeNode(ts.SyntaxKind.StringKeyword),
2823
headersType: undefinedType,
2924
pathParamsType: undefinedType,
3025
queryParamsType: undefinedType,
31-
contextTypeName: "MyContext",
3226
headersOptional: false,
3327
pathParamsOptional: false,
3428
queryParamsOptional: false,
@@ -38,18 +32,16 @@ describe("getVariableType", () => {
3832
expect(print(variablesType)).toMatchInlineSnapshot(`
3933
"{
4034
body: string;
41-
} & MyContext[\\"fetcherOptions\\"]"
35+
}"
4236
`);
4337
});
4438

4539
it("should have headers type declared", () => {
4640
const variablesType = getVariablesType({
47-
withContextType: true,
4841
requestBodyType: undefinedType,
4942
headersType: createType("Headers", "Foo"),
5043
pathParamsType: undefinedType,
5144
queryParamsType: undefinedType,
52-
contextTypeName: "MyContext",
5345
headersOptional: false,
5446
pathParamsOptional: false,
5547
queryParamsOptional: false,
@@ -59,18 +51,16 @@ describe("getVariableType", () => {
5951
expect(print(variablesType)).toMatchInlineSnapshot(`
6052
"{
6153
headers: Headers.Foo;
62-
} & MyContext[\\"fetcherOptions\\"]"
54+
}"
6355
`);
6456
});
6557

6658
it("should have pathParams type declared", () => {
6759
const variablesType = getVariablesType({
68-
withContextType: true,
6960
requestBodyType: undefinedType,
7061
headersType: undefinedType,
7162
pathParamsType: f.createKeywordTypeNode(ts.SyntaxKind.NumberKeyword),
7263
queryParamsType: undefinedType,
73-
contextTypeName: "MyContext",
7464
headersOptional: false,
7565
pathParamsOptional: false,
7666
queryParamsOptional: false,
@@ -80,18 +70,16 @@ describe("getVariableType", () => {
8070
expect(print(variablesType)).toMatchInlineSnapshot(`
8171
"{
8272
pathParams: number;
83-
} & MyContext[\\"fetcherOptions\\"]"
73+
}"
8474
`);
8575
});
8676

8777
it("should have queryParams type declared", () => {
8878
const variablesType = getVariablesType({
89-
withContextType: true,
9079
requestBodyType: undefinedType,
9180
headersType: undefinedType,
9281
pathParamsType: undefinedType,
9382
queryParamsType: createType("QueryParams", "Foo"),
94-
contextTypeName: "MyContext",
9583
headersOptional: false,
9684
pathParamsOptional: false,
9785
queryParamsOptional: false,
@@ -101,37 +89,31 @@ describe("getVariableType", () => {
10189
expect(print(variablesType)).toMatchInlineSnapshot(`
10290
"{
10391
queryParams: QueryParams.Foo;
104-
} & MyContext[\\"fetcherOptions\\"]"
92+
}"
10593
`);
10694
});
10795

10896
it("should ignore empty type", () => {
10997
const variablesType = getVariablesType({
110-
withContextType: true,
11198
requestBodyType: undefinedType,
11299
headersType: undefinedType,
113100
pathParamsType: undefinedType,
114101
queryParamsType: f.createTypeLiteralNode([]), // = {}
115-
contextTypeName: "MyContext",
116102
headersOptional: false,
117103
pathParamsOptional: false,
118104
queryParamsOptional: false,
119105
requestBodyOptional: false,
120106
});
121107

122-
expect(print(variablesType)).toMatchInlineSnapshot(
123-
`"MyContext[\\"fetcherOptions\\"]"`
124-
);
108+
expect(print(variablesType)).toMatchInlineSnapshot(`"void"`);
125109
});
126110

127111
it("should combine types", () => {
128112
const variablesType = getVariablesType({
129-
withContextType: true,
130113
requestBodyType: createType("RequestBody", "Pet"),
131114
headersType: createType("Headers", "Pet"),
132115
pathParamsType: createType("PathParams", "Pet"),
133116
queryParamsType: createType("QueryParams", "Pet"),
134-
contextTypeName: "MyContext",
135117
headersOptional: false,
136118
pathParamsOptional: false,
137119
queryParamsOptional: false,
@@ -144,18 +126,16 @@ describe("getVariableType", () => {
144126
headers: Headers.Pet;
145127
pathParams: PathParams.Pet;
146128
queryParams: QueryParams.Pet;
147-
} & MyContext[\\"fetcherOptions\\"]"
129+
}"
148130
`);
149131
});
150132

151133
it("should mark types as optional", () => {
152134
const variablesType = getVariablesType({
153-
withContextType: true,
154135
requestBodyType: createType("RequestBody", "Pet"),
155136
headersType: createType("Headers", "Pet"),
156137
pathParamsType: createType("PathParams", "Pet"),
157138
queryParamsType: createType("QueryParams", "Pet"),
158-
contextTypeName: "MyContext",
159139
headersOptional: true,
160140
pathParamsOptional: true,
161141
queryParamsOptional: true,
@@ -168,7 +148,7 @@ describe("getVariableType", () => {
168148
headers?: Headers.Pet;
169149
pathParams?: PathParams.Pet;
170150
queryParams?: QueryParams.Pet;
171-
} & MyContext[\\"fetcherOptions\\"]"
151+
}"
172152
`);
173153
});
174154
});

0 commit comments

Comments
 (0)