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; + } +}