From a0f252ba1834e13fd47949e7a4412ea3d39392be Mon Sep 17 00:00:00 2001
From: Armando Belardo <11140328+armandobelardo@users.noreply.github.com>
Date: Wed, 26 Jun 2024 08:25:44 -0400
Subject: [PATCH] feat: implement reference generation in generator-cli (#1070)
---
.../basic-reference.test.ts.snap | 184 ++++++++++++++++++
.../src/__test__/basic-reference.test.ts | 8 +
.../fixtures/basic-reference/reference.json | 84 ++++++++
.../src/__test__/testGenerateReference.ts | 35 ++++
clis/generator-cli/src/cli.ts | 33 ++++
.../src/configuration/loadReferenceConfig.ts | 12 ++
.../src/reference/ReferenceGenerator.ts | 86 ++++++++
7 files changed, 442 insertions(+)
create mode 100644 clis/generator-cli/src/__test__/__snapshots__/basic-reference.test.ts.snap
create mode 100644 clis/generator-cli/src/__test__/basic-reference.test.ts
create mode 100644 clis/generator-cli/src/__test__/fixtures/basic-reference/reference.json
create mode 100644 clis/generator-cli/src/__test__/testGenerateReference.ts
create mode 100644 clis/generator-cli/src/configuration/loadReferenceConfig.ts
create mode 100644 clis/generator-cli/src/reference/ReferenceGenerator.ts
diff --git a/clis/generator-cli/src/__test__/__snapshots__/basic-reference.test.ts.snap b/clis/generator-cli/src/__test__/__snapshots__/basic-reference.test.ts.snap
new file mode 100644
index 0000000000..d61a0aae51
--- /dev/null
+++ b/clis/generator-cli/src/__test__/__snapshots__/basic-reference.test.ts.snap
@@ -0,0 +1,184 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`basic-reference > basic-reference > generate reference file 1`] = `
+"## Accounts
+This package contains all endpoints on accounts...
+client.accounts.get()
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+Some description specific to the endpoint about accounts, etc. etc.
+
+It can also be multi-line.
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+\`\`\`python
+client.accounts.get(account_id="ID")
+\`\`\`
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**account_id:** \`str\`
+
+The ID of the account to retrieve.
+
+This is a multi-line description as well.
+
+
+
+
+
+
+
+
+
+
+
+## Users
+This package contains all endpoints on users...
+client.users.get()
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+Some description specific to the endpoint about users, etc. etc.
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+\`\`\`python
+client.users.get(user_id="ID", account_id="ACCOUNT_ID")
+\`\`\`
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**user_id:** \`str\` — The ID of the user to retrieve.
+
+
+
+
+
+-
+
+**account_id:** \`str\` — The ID of the account to retrieve the user from.
+
+
+
+
+
+
+
+
+
+
+
+client.users.update()
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+Some description specific to the endpoint about users, etc. etc.
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+\`\`\`python
+client.users.get(update=User(id="ID")
+\`\`\`
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**update:** \`User\` — The updated user object to send to the server.
+
+
+
+
+
+
+
+
+
+
+"
+`;
diff --git a/clis/generator-cli/src/__test__/basic-reference.test.ts b/clis/generator-cli/src/__test__/basic-reference.test.ts
new file mode 100644
index 0000000000..427a212f17
--- /dev/null
+++ b/clis/generator-cli/src/__test__/basic-reference.test.ts
@@ -0,0 +1,8 @@
+import { testGenerateReference } from "./testGenerateReference";
+
+describe("basic-reference", () => {
+ testGenerateReference({
+ fixtureName: "basic-reference",
+ referenceConfigFilename: "reference.json",
+ });
+});
diff --git a/clis/generator-cli/src/__test__/fixtures/basic-reference/reference.json b/clis/generator-cli/src/__test__/fixtures/basic-reference/reference.json
new file mode 100644
index 0000000000..5333a24d45
--- /dev/null
+++ b/clis/generator-cli/src/__test__/fixtures/basic-reference/reference.json
@@ -0,0 +1,84 @@
+{
+ "sections": [
+ {
+ "title": "Accounts",
+ "description": "This package contains all endpoints on accounts...",
+ "endpoints": [
+ {
+ "title": {
+ "snippetParts": [
+ {
+ "text": "client.accounts.get()",
+ "location": { "path": "./src/accounts.py" }
+ }
+ ]
+ },
+ "description": "Some description specific to the endpoint about accounts, etc. etc.\n\nIt can also be multi-line.",
+ "snippet": "client.accounts.get(account_id=\"ID\")",
+ "parameters": [
+ {
+ "name": "account_id",
+ "type": "str",
+ "description": "The ID of the account to retrieve.\n\nThis is a multi-line description as well.",
+ "required": true
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "title": "Users",
+ "description": "This package contains all endpoints on users...",
+ "endpoints": [
+ {
+ "title": {
+ "snippetParts": [
+ {
+ "text": "client.users.get()",
+ "location": { "path": "./src/users.py" }
+ }
+ ]
+ },
+ "description": "Some description specific to the endpoint about users, etc. etc.",
+ "snippet": "client.users.get(user_id=\"ID\", account_id=\"ACCOUNT_ID\")",
+ "parameters": [
+ {
+ "name": "user_id",
+ "type": "str",
+ "description": "The ID of the user to retrieve.",
+ "required": true
+ },
+ {
+ "name": "account_id",
+ "type": "str",
+ "description": "The ID of the account to retrieve the user from.",
+ "required": true
+ }
+ ]
+ },
+ {
+ "title": {
+ "snippetParts": [
+ {
+ "text": "client.users.update()",
+ "location": { "path": "./src/users.py" }
+ }
+ ]
+ },
+ "description": "Some description specific to the endpoint about users, etc. etc.",
+ "snippet": "client.users.get(update=User(id=\"ID\")",
+ "parameters": [
+ {
+ "name": "update",
+ "type": "User",
+ "location": { "path": "./src/users.py" },
+ "description": "The updated user object to send to the server.",
+ "required": true
+ }
+ ]
+ }
+ ]
+ }
+ ],
+ "language": "PYTHON"
+}
diff --git a/clis/generator-cli/src/__test__/testGenerateReference.ts b/clis/generator-cli/src/__test__/testGenerateReference.ts
new file mode 100644
index 0000000000..f44063a01b
--- /dev/null
+++ b/clis/generator-cli/src/__test__/testGenerateReference.ts
@@ -0,0 +1,35 @@
+import execa from "execa";
+import path from "path";
+
+const FIXTURES_PATH = path.join(__dirname, "fixtures");
+
+export function testGenerateReference({
+ fixtureName,
+ referenceConfigFilename,
+}: {
+ fixtureName: string;
+ referenceConfigFilename: string;
+}): void {
+ // eslint-disable-next-line vitest/valid-title
+ describe(fixtureName, () => {
+ it("generate reference file", async () => {
+ const absolutePathToReferenceConfig = getAbsolutePathToFixtureFile({
+ fixtureName,
+ filepath: referenceConfigFilename,
+ });
+ const args = [
+ path.join(__dirname, "../../dist/cli.cjs"),
+ "generate-reference",
+ "--config",
+ absolutePathToReferenceConfig,
+ ];
+
+ const { stdout } = await execa("node", args);
+ expect(stdout).toMatchSnapshot();
+ });
+ });
+}
+
+function getAbsolutePathToFixtureFile({ fixtureName, filepath }: { fixtureName: string; filepath: string }): string {
+ return path.join(FIXTURES_PATH, fixtureName, filepath);
+}
diff --git a/clis/generator-cli/src/cli.ts b/clis/generator-cli/src/cli.ts
index 5761cba264..33b6e966dc 100644
--- a/clis/generator-cli/src/cli.ts
+++ b/clis/generator-cli/src/cli.ts
@@ -5,8 +5,10 @@ import path from "path";
import { hideBin } from "yargs/helpers";
import yargs from "yargs/yargs";
import { loadReadmeConfig } from "./configuration/loadReadmeConfig";
+import { loadReferenceConfig } from "./configuration/loadReferenceConfig";
import { ReadmeGenerator } from "./readme/ReadmeGenerator";
import { ReadmeParser } from "./readme/ReadmeParser";
+import { ReferenceGenerator } from "./reference/ReferenceGenerator";
void yargs(hideBin(process.argv))
.scriptName(process.env.CLI_NAME ?? "generator-cli")
@@ -48,6 +50,37 @@ void yargs(hideBin(process.argv))
process.exit(0);
},
)
+ .command(
+ "generate-reference",
+ "Generate an SDK reference (`reference.md`) using the provided configuration file.",
+ (argv) =>
+ argv
+ .option("config", {
+ string: true,
+ requred: true,
+ })
+ .option("output", {
+ string: true,
+ requred: false,
+ }),
+ async (argv) => {
+ if (argv.config == null) {
+ process.stderr.write("missing required arguments; please specify the --config flag\n");
+ process.exit(1);
+ }
+ const wd = cwd();
+ const referenceConfig = await loadReferenceConfig({
+ absolutePathToConfig: resolve(wd, argv.config),
+ });
+ const generator = new ReferenceGenerator({
+ referenceConfig,
+ });
+ await generator.generate({
+ output: await createWriteStream(argv.output),
+ });
+ process.exit(0);
+ },
+ )
.demandCommand()
.showHelpOnFail(true)
.parse();
diff --git a/clis/generator-cli/src/configuration/loadReferenceConfig.ts b/clis/generator-cli/src/configuration/loadReferenceConfig.ts
new file mode 100644
index 0000000000..f0cccf9f2d
--- /dev/null
+++ b/clis/generator-cli/src/configuration/loadReferenceConfig.ts
@@ -0,0 +1,12 @@
+import { AbsoluteFilePath } from "@fern-api/fs-utils";
+import { readFile } from "fs/promises";
+import { FernGeneratorCli } from "./generated";
+
+export async function loadReferenceConfig({
+ absolutePathToConfig,
+}: {
+ absolutePathToConfig: AbsoluteFilePath;
+}): Promise {
+ const rawContents = await readFile(absolutePathToConfig, "utf8");
+ return JSON.parse(rawContents);
+}
diff --git a/clis/generator-cli/src/reference/ReferenceGenerator.ts b/clis/generator-cli/src/reference/ReferenceGenerator.ts
new file mode 100644
index 0000000000..043b5dcd99
--- /dev/null
+++ b/clis/generator-cli/src/reference/ReferenceGenerator.ts
@@ -0,0 +1,86 @@
+import fs from "fs";
+import { FernGeneratorCli } from "../configuration/generated";
+import {
+ EndpointReference,
+ LinkedText,
+ ParameterReference,
+ ReferenceSection,
+ RelativeLocation,
+} from "../configuration/generated/api";
+import { StreamWriter, StringWriter, Writer } from "../utils/Writer";
+
+export class ReferenceGenerator {
+ private referenceConfig: FernGeneratorCli.ReferenceConfig;
+
+ constructor({ referenceConfig }: { referenceConfig: FernGeneratorCli.ReferenceConfig }) {
+ this.referenceConfig = referenceConfig;
+ }
+
+ public async generate({ output }: { output: fs.WriteStream }): Promise {
+ const writer = new StreamWriter(output);
+ for (const section of this.referenceConfig.sections) {
+ this.writeSection({ section, writer });
+ }
+ writer.end();
+ }
+
+ private writeSection({ section, writer }: { section: ReferenceSection; writer: Writer }): void {
+ writer.writeLine(`## ${section.title}`);
+ writer.writeLine(`${section.description}`);
+ for (const endpoint of section.endpoints) {
+ this.writeEndpoint({ endpoint, writer });
+ }
+ }
+
+ private writeEndpoint({ endpoint, writer }: { endpoint: EndpointReference; writer: Writer }): void {
+ const stringWriter = new StringWriter();
+ if (endpoint.description !== undefined) {
+ stringWriter.writeLine(
+ `#### 📝 Description\n\n${this.writeIndentedBlock(this.writeIndentedBlock(endpoint.description))}\n`,
+ );
+ }
+ stringWriter.writeLine(
+ `#### 🔌 Usage\n\n${this.writeIndentedBlock(
+ this.writeIndentedBlock(
+ "```" + this.referenceConfig.language.toLowerCase() + "\n" + endpoint.snippet + "\n```",
+ ),
+ )}\n`,
+ );
+ if (endpoint.parameters.length > 0) {
+ stringWriter.writeLine(
+ `#### ⚙️ Parameters\n\n${this.writeIndentedBlock(
+ endpoint.parameters
+ .map((parameter) => this.writeIndentedBlock(this.writeParameter(parameter)))
+ .join("\n\n"),
+ )}\n`,
+ );
+ }
+
+ writer.writeLine(
+ `${this.wrapInLinksAndJoin(endpoint.title.snippetParts)}
`,
+ );
+ writer.writeLine(this.writeIndentedBlock(stringWriter.toString()));
+ writer.writeLine(" \n");
+ }
+
+ private writeParameter(parameter: ParameterReference): string {
+ const desc = parameter.description?.match(/[^\r\n]+/g)?.length;
+ const containsLineBreak = desc !== undefined && desc > 1;
+ return `**${parameter.name}:** \`${this.wrapInLink(parameter.type, parameter.location)}\` ${
+ parameter.description !== undefined ? (containsLineBreak ? "\n\n" : "— ") + parameter.description : ""
+ }
+ `;
+ }
+
+ private writeIndentedBlock(content: string): string {
+ return `\n- \n\n${content}\n
\n
`;
+ }
+
+ private wrapInLinksAndJoin(content: LinkedText[]): string {
+ return content.map(({ text, location }) => this.wrapInLink(text, location)).join("");
+ }
+
+ private wrapInLink(content: string, link?: RelativeLocation) {
+ return link !== undefined ? `${content}` : content;
+ }
+}