Skip to content

Commit 1e947c3

Browse files
authored
Merge pull request #3 from moznion/default_header
Support default header
2 parents 64c8bc5 + ce4b5ad commit 1e947c3

File tree

11 files changed

+493
-39
lines changed

11 files changed

+493
-39
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
node_modules/
22
dist/
3-
src/test_resources/generated_client.ts
3+
src/test_resources/generated_client*.ts
44

README.md

Lines changed: 137 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -30,18 +30,21 @@ npx openapi-fetch-gen --input ./schema.d.ts --output ./client.ts
3030

3131
Options:
3232

33-
- `-V, --version output the version number`
34-
- `-i, --input <path> path to input OpenAPI TypeScript definition file`
35-
- `-o, --output <path> path to output generated client file (default: "./client.ts")`
36-
- `-h, --help display help for command`
33+
```
34+
-V, --version output the version number
35+
-i, --input <path> path to input OpenAPI TypeScript definition file
36+
-o, --output <path> path to output generated client file (default: "./client.ts")
37+
--default-headers <comma_separated_names_of_headers> header names so that the generated client includes the default HTTP headers across all endpoints (default: "")
38+
-h, --help display help for command
39+
```
3740

38-
## Example
41+
### Example
3942

40-
Please refer the [examples](./examples/).
43+
Please refer to the [examples](./examples/).
4144

4245
`schema.d.ts` is generated from `schema.yaml` by `openapi-typescript`, and `generated_client.ts` is generated by this tool according to the `schema.d.ts`.
4346

44-
FYI, the client user would be able to use it as like the following:
47+
FYI, you can use the generated client as follows:
4548

4649
```typescript
4750
import { Client } from "./generated_client";
@@ -62,6 +65,133 @@ async function doSomething() {
6265
}
6366
```
6467

68+
### Default HTTP Headers
69+
70+
This tool provides a `--default-headers` CLI option to configure the generated client to use default HTTP headers.
71+
72+
For example, when you specify:
73+
74+
```bash
75+
npx openapi-fetch-gen \
76+
--input ./schema.d.ts \
77+
--output ./client.ts \
78+
--default-headers "Authorization, Application-Version"
79+
```
80+
81+
the `Client` constructor accepts those default values:
82+
83+
```typescript
84+
type DefaultHeaders = Record<'Authorization' | 'Application-Version', string>;
85+
86+
export class Client {
87+
constructor(clientOptions: ClientOptions, defaultHeaders: DefaultHeaders) { ... }
88+
}
89+
```
90+
91+
Endpoint methods that require those headers no longer need the default values to be passed explicitly.
92+
93+
For example, given this schema:
94+
95+
```typescript
96+
"/users/bulk/{jobId}": {
97+
get: {
98+
parameters: {
99+
query?: never;
100+
header: {
101+
/** @description Authorization Header */
102+
Authorization: string;
103+
/** @description Application version */
104+
"Application-Version": string;
105+
/** @description Identifier of something */
106+
"Something-Id": string;
107+
};
108+
path: {
109+
/** @description Bulk import job identifier */
110+
jobId: string;
111+
};
112+
cookie?: never;
113+
};
114+
```
115+
116+
When `--default-headers` is not specified, the generated client method looks like this:
117+
118+
```typescript
119+
async getUsersBulkJobid(params: {
120+
header: {
121+
Authorization: string;
122+
"Application-Version": string;
123+
"Something-Id": string;
124+
};
125+
path: { jobId: string };
126+
}) {
127+
...
128+
}
129+
```
130+
131+
This requires you to pass all header values explicitly.
132+
133+
When you do specify `--default-headers`, the signature becomes:
134+
135+
```typescript
136+
async getUsersBulkJobid(
137+
params: keyof Omit<
138+
{
139+
Authorization: string;
140+
"Application-Version": string;
141+
"Something-Id": string;
142+
},
143+
keyof DefaultHeaders
144+
> extends never
145+
? {
146+
header?: {
147+
Authorization: string;
148+
"Application-Version": string;
149+
"Something-Id": string;
150+
};
151+
path: { jobId: string };
152+
}
153+
: {
154+
header:
155+
| Omit<
156+
{
157+
Authorization: string;
158+
"Application-Version": string;
159+
"Something-Id": string;
160+
},
161+
keyof DefaultHeaders
162+
>
163+
| {
164+
Authorization: string;
165+
"Application-Version": string;
166+
"Something-Id": string;
167+
};
168+
path: { jobId: string };
169+
},
170+
) {
171+
...
172+
}
173+
```
174+
175+
That signature means you can:
176+
177+
- **Omit** the defaulted headers and only pass additional ones (here, `Something-Id`):
178+
179+
```typescript
180+
client.getUsersBulkJobid({header: {"Something-Id": "foobar"}, path: {jobId: "123"}});
181+
```
182+
183+
- **Override** all headers, including the defaults:
184+
185+
```typescript
186+
client.getUsersBulkJobid({header: {"Authorization": "foo", "Application-Version": "bar", "Something-Id": "foobar"}, path: {jobId: "123"}});
187+
```
188+
189+
If your default headers cover **all** required headers for the endpoint (e.g. `--default-headers 'Authorization, Application-Version, Something-Id'`), you can omit the `header` parameter entirely:
190+
191+
```typescript
192+
client.getUsersBulkJobid({path: {jobId: "123"}});
193+
```
194+
65195
## License
66196
67197
MIT

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,9 @@
1717
"build": "pnpm clean && tsc",
1818
"lint": "biome check src",
1919
"fix": "biome check --write src",
20-
"test": "vitest run",
20+
"test": "pnpm test:clean && vitest run",
2121
"test:watch": "vitest",
22+
"test:clean": "rm -f ./src/test_resources/generated_client*",
2223
"generate_test_resource": "openapi-typescript -o ./src/test_resources/schema.d.ts ./src/test_resources/schema.yaml"
2324
},
2425
"bin": {

pnpm-workspace.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
ignoredBuiltDependencies:
2+
- esbuild
3+
4+
onlyBuiltDependencies:
5+
- '@biomejs/biome'

src/cli.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,20 +23,31 @@ program
2323
"path to output generated client file",
2424
"./client.ts",
2525
)
26+
.option(
27+
"--default-headers <comma_separated_names_of_headers>",
28+
"header names so that the generated client includes the default HTTP headers across all endpoints",
29+
"",
30+
)
2631
.parse(process.argv);
2732

2833
const options = program.opts();
2934

3035
try {
3136
const inputPath = path.resolve(options["input"]);
3237
const outputPath = path.resolve(options["output"]);
38+
const defaultHeaderNamesString = options["defaultHeaders"] as string;
3339

3440
if (!fs.existsSync(inputPath)) {
3541
console.error(`Error: Input file not found: ${inputPath}`);
3642
process.exit(1);
3743
}
3844

39-
const clientCode = generateClient(inputPath);
45+
const defaultHeaderNames = defaultHeaderNamesString
46+
.split(",")
47+
.map((h) => h.trim())
48+
.filter((h) => h !== "");
49+
50+
const clientCode = generateClient(inputPath, defaultHeaderNames);
4051

4152
fs.writeFileSync(outputPath, clientCode);
4253
console.log(`Successfully generated client at: ${outputPath}`);

src/index.ts

Lines changed: 76 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,10 @@ import {
1515
* Generates a TypeScript API client using openapi-fetch based on TypeScript interface definitions
1616
* generated by openapi-typescript.
1717
*/
18-
export function generateClient(schemaFilePath: string): string {
18+
export function generateClient(
19+
schemaFilePath: string,
20+
defaultHeaderNames: string[],
21+
): string {
1922
const project = new Project({
2023
compilerOptions: {
2124
target: ScriptTarget.Latest,
@@ -29,7 +32,11 @@ export function generateClient(schemaFilePath: string): string {
2932
throw new Error(`Interface "paths" not found in ${schemaFilePath}`);
3033
}
3134

32-
return generateClientCode(pathsInterface, path.basename(schemaFilePath));
35+
return generateClientCode(
36+
pathsInterface,
37+
path.basename(schemaFilePath),
38+
defaultHeaderNames,
39+
);
3340
}
3441

3542
function findInterface(
@@ -46,15 +53,23 @@ interface EndpointInfo {
4653
commentLines: string[];
4754
paramsType: string | null;
4855
bodyType: string | null;
56+
headerExists: boolean;
4957
}
5058

5159
function generateClientCode(
5260
pathsInterface: InterfaceDeclaration,
5361
schemaFileName: string,
62+
defaultHeaderNames: string[],
5463
): string {
55-
const endpoints = extractEndpointsInfo(pathsInterface);
64+
const defaultHeaderExists = defaultHeaderNames.length > 0;
65+
66+
const endpoints = extractEndpointsInfo(pathsInterface, defaultHeaderExists);
5667

57-
const clientClass = generateClientClass(endpoints);
68+
const defaultHeaderType =
69+
defaultHeaderNames.length > 0
70+
? `type DefaultHeaders = Record<${defaultHeaderNames.map((n) => `"${n}"`).join("|")}, string>\n`
71+
: "";
72+
const clientClass = generateClientClass(endpoints, defaultHeaderExists);
5873

5974
const code = [
6075
"// THIS FILE IS AUTO-GENERATED BY openapi-fetch-gen.",
@@ -63,6 +78,7 @@ function generateClientCode(
6378
`import createClient, { type ClientOptions } from "openapi-fetch";`,
6479
`import type { paths } from "./${schemaFileName.replace(/\.d\.ts$/, "")}"; // generated by openapi-typescript`,
6580
"",
81+
defaultHeaderType,
6682
clientClass,
6783
].join("\n");
6884

@@ -74,6 +90,7 @@ function generateClientCode(
7490

7591
function extractEndpointsInfo(
7692
pathsInterface: InterfaceDeclaration,
93+
defaultHeaderExists: boolean,
7794
): EndpointInfo[] {
7895
const endpoints: EndpointInfo[] = [];
7996

@@ -121,16 +138,41 @@ function extractEndpointsInfo(
121138
.getProperties()
122139
.map((prop) => {
123140
const propType = prop.getTypeAtLocation(decl);
124-
const typeText = propType.getText();
125-
if (typeText === "never") {
126-
return "";
141+
const text = propType.getText();
142+
const name = prop.getName();
143+
if (text === "never") {
144+
return { name, text: "" };
127145
}
128-
return `${prop.getName()}: ${typeText}`;
146+
return { name, text };
129147
})
130-
.filter((typeText) => typeText !== "")
131-
.join(", ");
132-
if (paramTypes !== "") {
133-
paramsType = `{${paramTypes}}`;
148+
.filter((kv) => kv["text"] !== "");
149+
150+
const headerType = paramTypes.find((kv) => kv.name === "header");
151+
if (defaultHeaderExists && headerType) {
152+
const nonHeaderParams = paramTypes
153+
.filter((kv) => kv.name !== "header")
154+
.map((kv) => `${kv.name}: ${kv.text}`)
155+
.join("\n");
156+
157+
paramsType = `keyof Omit<${headerType["text"]}, keyof DefaultHeaders> extends never ?
158+
{
159+
header?: ${headerType["text"]},
160+
${nonHeaderParams}
161+
} :
162+
{
163+
header:
164+
| Omit<${headerType["text"]}, keyof DefaultHeaders>
165+
| ${headerType["text"]},
166+
${nonHeaderParams}
167+
}
168+
`;
169+
} else {
170+
const params = paramTypes
171+
.map((kv) => `${kv.name}: ${kv.text}`)
172+
.join("\n");
173+
if (params !== "") {
174+
paramsType = `{${params}}`;
175+
}
134176
}
135177

136178
const requestBodyProp = decl.getType().getProperty("requestBody");
@@ -185,22 +227,28 @@ function extractEndpointsInfo(
185227
commentLines,
186228
paramsType,
187229
bodyType: requestBodyType,
230+
headerExists: headerType !== undefined,
188231
});
189232
}
190233
}
191234

192235
return endpoints;
193236
}
194237

195-
function generateClientClass(endpoints: EndpointInfo[]): string {
238+
function generateClientClass(
239+
endpoints: EndpointInfo[],
240+
defaultHeaderExists: boolean,
241+
): string {
196242
const classCode = [
197-
"export class Client {",
198-
" private readonly client;",
199-
"",
200-
" constructor(clientOptions: ClientOptions) {",
201-
" this.client = createClient<paths>(clientOptions);",
202-
" }",
203-
"",
243+
`export class Client {
244+
private readonly client;
245+
${defaultHeaderExists ? "private readonly defaultHeaders: DefaultHeaders;" : ""}
246+
247+
constructor(clientOptions: ClientOptions${defaultHeaderExists ? ", defaultHeaders: DefaultHeaders" : ""}) {
248+
this.client = createClient<paths>(clientOptions);
249+
${defaultHeaderExists ? "this.defaultHeaders = defaultHeaders;" : ""}
250+
}
251+
`,
204252
];
205253

206254
// Generate class methods for each endpoint
@@ -249,7 +297,14 @@ function generateClientClass(endpoints: EndpointInfo[]): string {
249297
);
250298

251299
if (paramsType) {
252-
classCode.push(" params,");
300+
if (defaultHeaderExists && endpoint.headerExists) {
301+
classCode.push(`params: {
302+
...params,
303+
header: {...this.defaultHeaders, ...params.header},
304+
},`);
305+
} else {
306+
classCode.push("params,");
307+
}
253308
}
254309

255310
if (bodyType) {

0 commit comments

Comments
 (0)