Skip to content

Commit 0d054f4

Browse files
authored
feat: github as openapi source (#54)
* Add github source option * Fix command options * Add pull request support * Update clipanion * Deal with provided pull request (--pr=42) * Add Spinner * Leaner spinner * Remove unused import * Run npm audit fix * Rename `branch` to `ref` to be more accurate * Add support for env var * Update snapshot
1 parent 3ab7010 commit 0d054f4

18 files changed

+833
-154
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ export default defineConfig({
126126
source: "github",
127127
owner: "fabien0102",
128128
repository: "openapi-codegen",
129-
branch: "main",
129+
ref: "main",
130130
specPath: "examples/spec.yaml",
131131
},
132132

cli/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export default defineConfig({
1818
source: "github",
1919
owner: "fabien0102",
2020
repository: "openapi-codegen",
21-
branch: "main",
21+
ref: "main",
2222
specPath: "examples/spec.yaml",
2323
},
2424

cli/examples/openapi-codegen.config.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { defineConfig } from "../lib/index.js";
2-
import { generateFetchers } from "@openapi-codegen/typescript";
32

43
export default defineConfig({
54
withFile: {
@@ -22,4 +21,17 @@ export default defineConfig({
2221
console.log(context);
2322
},
2423
},
24+
withGithub: {
25+
from: {
26+
source: "github",
27+
owner: "fabien0102",
28+
ref: "main",
29+
repository: "openapi-codegen",
30+
specPath: "cli/examples/petstore.json",
31+
},
32+
outputDir: "petstore",
33+
to: async (context) => {
34+
console.log(context);
35+
},
36+
},
2537
});

cli/package-lock.json

Lines changed: 20 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

cli/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@
3838
"case": "^1.6.3",
3939
"chalk": "^5.0.0",
4040
"cli-highlight": "^2.1.11",
41-
"clipanion": "^3.2.0-rc.3",
41+
"clipanion": "^3.2.0-rc.10",
4242
"fs-extra": "^10.0.0",
4343
"got": "^12.0.0",
4444
"got-fetch": "^5.0.2",
@@ -52,6 +52,7 @@
5252
"slash": "^4.0.0",
5353
"swagger2openapi": "^7.0.8",
5454
"tslib": "^2.3.1",
55+
"typanion": "^3.7.1",
5556
"typescript": "^4.6.2"
5657
},
5758
"devDependencies": {

cli/src/commands/GenerateCommand.ts

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,53 +19,64 @@ const __filename = fileURLToPath(import.meta.url);
1919
export class GenerateCommand extends Command {
2020
config = Option.String(`-c,--config`, {
2121
description: "Configuration file path",
22+
env: "OPENAPI_CODEGEN_CONFIG",
2223
});
2324

2425
namespace = Option.String();
2526

2627
source = Option.String(`--source`, {
2728
description: "Source of the spec (file, url or github)",
29+
env: "OPENAPI_CODEGEN_SOURCE",
2830
validator: t.isEnum(["file", "url", "github"]),
2931
});
3032

3133
// source=file options
3234
relativePath = Option.String(`--relativePath`, {
3335
description: "[source=file] Relative path of the spec file",
36+
env: "OPENAPI_CODEGEN_FILE_PATH",
3437
});
3538

3639
// source=url options
3740
url = Option.String("--url", {
3841
description: "[source=url] URL of the spec file",
42+
env: "OPENAPI_CODEGEN_URL",
3943
});
4044
method = Option.String("--method", {
4145
description: "[source=url] HTTP Method",
46+
env: "OPENAPI_CODEGEN_URL_METHOD",
4247
validator: t.isEnum(["get", "post"]),
4348
});
4449

4550
// source=github options
4651
owner = Option.String("--owner", {
4752
description: "[source=github] Owner of the repository",
53+
env: "OPENAPI_CODEGEN_GITHUB_OWNER",
4854
});
49-
repository = Option.String("--repository --repo", {
55+
repository = Option.String("--repository,--repo", {
5056
description: "[source=github] Repository name",
57+
env: "OPENAPI_CODEGEN_GITHUB_REPOSITORY",
5158
});
52-
branch = Option.String("-b --branch", {
53-
description: "[source=github] Branch name",
59+
ref = Option.String("--ref", {
60+
description: "[source=github] Git reference (commit sha, branch or tag)",
61+
env: "OPENAPI_CODEGEN_GITHUB_REF",
5462
});
5563
specPath = Option.String("--specPath", {
5664
description: "[source=github] OpenAPI specs file path",
65+
env: "OPENAPI_CODEGEN_GITHUB_SPEC_PATH",
5766
});
58-
pullRequest = Option.String("--pr --pull-request", {
59-
description:
60-
"[source=github] Select a specific pull-request instead of a branch",
67+
pullRequest = Option.String("--pr,--pull-request", {
68+
description: "[source=github] Select a specific pull-request as ref",
69+
env: "OPENAPI_CODEGEN_GITHUB_PULL_REQUEST",
70+
validator: t.isNumber(),
71+
tolerateBoolean: true,
6172
});
6273

6374
static paths = [["gen"], ["generate"], Command.Default];
6475
static usage = Command.Usage({
6576
description: "Generate types & components from an OpenAPI file",
6677
examples: [
6778
[`From a config key`, `$0 gen myapi`],
68-
[`With some override`, `$0 gen myapi --branch awesome-feature`],
79+
[`With some override`, `$0 gen myapi --ref awesome-feature`],
6980
],
7081
});
7182

@@ -159,16 +170,16 @@ export class GenerateCommand extends Command {
159170
return {
160171
...config.from,
161172
owner: this.owner ?? config.from.owner,
162-
branch: this.branch ?? config.from.branch,
173+
ref: this.ref ?? config.from.ref,
163174
repository: this.repository ?? config.from.repository,
164175
specPath: this.specPath ?? config.from.specPath,
165176
};
166177
} else {
167178
if (!this.owner) {
168179
throw new UsageError("--owner argument is missing");
169180
}
170-
if (!this.branch) {
171-
throw new UsageError("--branch argument is missing");
181+
if (!this.ref && !this.pullRequest) {
182+
throw new UsageError("--ref argument is missing");
172183
}
173184
if (!this.repository) {
174185
throw new UsageError("--repository argument is missing");
@@ -179,7 +190,7 @@ export class GenerateCommand extends Command {
179190

180191
return {
181192
source: "github",
182-
branch: this.branch,
193+
ref: this.ref || "main", // Fallback for --pr mode
183194
owner: this.owner,
184195
repository: this.repository,
185196
specPath: this.specPath,
@@ -197,7 +208,24 @@ export class GenerateCommand extends Command {
197208
}
198209

199210
const config = configs[this.namespace];
200-
const sourceFile = await getOpenAPISourceFile(this.getFromOptions(config));
211+
const options = this.getFromOptions(config);
212+
if (options.source === "github" && this.pullRequest) {
213+
const { Prompt } = await import("../prompts/Prompt.js");
214+
const prompt = new Prompt();
215+
const token = await prompt.githubToken();
216+
const pullRequest = await prompt.githubPullRequest({
217+
...options,
218+
token,
219+
pullRequestNumber:
220+
typeof this.pullRequest === "number" ? this.pullRequest : undefined,
221+
});
222+
223+
options.ref = pullRequest.ref;
224+
options.owner = pullRequest.owner;
225+
options.repository = pullRequest.repository;
226+
}
227+
228+
const sourceFile = await getOpenAPISourceFile(options);
201229
const openAPIDocument = await parseOpenAPISourceFile(sourceFile);
202230
const prettierConfig = await prettier.resolveConfig(process.cwd());
203231

cli/src/commands/InitCommand.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,7 @@ export class InitCommand extends Command {
141141
? await this.askForFile()
142142
: source === "url"
143143
? await this.askForUrl()
144-
: await this.prompt.github();
144+
: await this.prompt.github("todo: inject the token");
145145

146146
const namespace = format.camel(
147147
await this.prompt.input({

cli/src/core/generateConfigProperty.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ describe("generateConfigProperty", () => {
7171
options: {
7272
from: {
7373
source: "github",
74-
branch: "main",
74+
ref: "main",
7575
owner: "fabien0102",
7676
repository: "openapi-codegen",
7777
specPath: "examples/petstore.json",
@@ -85,7 +85,7 @@ describe("generateConfigProperty", () => {
8585
"foo: {
8686
from: {
8787
source: \\"github\\",
88-
branch: \\"main\\",
88+
ref: \\"main\\",
8989
owner: \\"fabien0102\\",
9090
repository: \\"openapi-codegen\\",
9191
specPath: \\"examples/petstore.json\\"

cli/src/core/getOpenAPISourceFile.ts

Lines changed: 77 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { readFileSync } from "fs";
1+
import { UsageError } from "clipanion";
2+
import { readFileSync, unlinkSync } from "fs";
3+
import { HTTPError } from "got";
4+
import { homedir } from "os";
25
import { join, parse } from "path";
36
import { URL } from "url";
47
import { FromOptions, OpenAPISourceFile } from "../types";
@@ -39,9 +42,79 @@ export const getOpenAPISourceFile = async (
3942
return { text: file.body, format };
4043
}
4144

42-
case "github":
43-
// TODO
44-
return { text: "", format: "json" };
45+
case "github": {
46+
// Retrieve Github token
47+
const { Prompt } = await import("../prompts/Prompt.js");
48+
const prompt = new Prompt();
49+
50+
const token = await prompt.githubToken();
51+
52+
// Retrieve specs
53+
const { default: got } = await import("got");
54+
55+
try {
56+
const raw = await got
57+
.post("https://api.github.com/graphql", {
58+
headers: {
59+
"content-type": "application/json",
60+
"user-agent": "openapi-codegen",
61+
authorization: `bearer ${token}`,
62+
},
63+
body: JSON.stringify({
64+
query: `query {
65+
repository(name: "${options.repository}", owner: "${options.owner}") {
66+
object(expression: "${options.ref}:${options.specPath}") {
67+
... on Blob {
68+
text
69+
}
70+
}
71+
}
72+
}`,
73+
}),
74+
})
75+
.json<{
76+
data: { repository: { object: { text: string } | null } };
77+
errors?: [
78+
{
79+
message: string;
80+
}
81+
];
82+
}>();
83+
84+
prompt.close();
85+
if (raw.errors) {
86+
throw new UsageError(raw.errors[0].message);
87+
}
88+
if (raw.data.repository.object === null) {
89+
throw new UsageError(`No file found at "${options.specPath}"`);
90+
}
91+
92+
let format: OpenAPISourceFile["format"] = "yaml";
93+
if (options.specPath.toLowerCase().endsWith("json")) {
94+
format = "json";
95+
}
96+
97+
return { text: raw.data.repository.object.text, format };
98+
} catch (e) {
99+
if (
100+
e instanceof HTTPError &&
101+
e.response.statusCode === 401 &&
102+
!process.env.GITHUB_TOKEN
103+
) {
104+
const removeToken = await prompt.confirm(
105+
"Your token doesn't have the correct permissions, should we remove it?"
106+
);
107+
prompt.close();
108+
109+
if (removeToken) {
110+
const githubTokenPath = join(homedir(), ".openapi-codegen");
111+
unlinkSync(githubTokenPath);
112+
return await getOpenAPISourceFile(options);
113+
}
114+
}
115+
throw e;
116+
}
117+
}
45118
}
46119
};
47120

0 commit comments

Comments
 (0)