From 12b5bbb865f67ca0b17a331df926ae6eb20d94ea Mon Sep 17 00:00:00 2001 From: Alex McKinney Date: Fri, 31 Jan 2025 13:10:49 -0600 Subject: [PATCH] feat(go): Integrate TypeScript generator (#5816) --- generators/go-v2/ast/src/ast/File.ts | 28 ++++++++ generators/go-v2/ast/src/ast/Struct.ts | 14 ++-- .../ast/src/ast/__test__/Snippets.test.ts | 48 ++++++++++++++ .../__snapshots__/Snippets.test.ts.snap | 17 +++++ generators/go-v2/ast/src/ast/core/Writer.ts | 3 +- generators/go-v2/ast/src/ast/index.ts | 1 + .../src/context/AbstractGoGeneratorContext.ts | 15 ++++- generators/go-v2/ast/src/go.ts | 6 ++ generators/go-v2/ast/src/index.ts | 1 + generators/go-v2/base/package.json | 2 + .../base/src/cli/AbstractGoGeneratorCli.ts | 24 +++++++ generators/go-v2/base/src/cli/index.ts | 1 + generators/go-v2/base/src/index.ts | 10 ++- generators/go-v2/base/src/project/GoFile.ts | 49 ++++++++++++++ .../go-v2/base/src/project/GoProject.ts | 58 ++++++++++++++++- generators/go-v2/base/src/project/index.ts | 1 + generators/go-v2/base/tsconfig.json | 1 + generators/go-v2/sdk/package.json | 12 +++- generators/go-v2/sdk/src/SdkCustomConfig.ts | 5 ++ generators/go-v2/sdk/src/SdkGeneratorCli.ts | 64 +++++++++++++++++++ .../go-v2/sdk/src/SdkGeneratorContext.ts | 22 +++++++ generators/go-v2/sdk/src/cli.ts | 6 +- .../sdk/src/wiretest/WireTestGenerator.ts | 39 +++++++++++ generators/go-v2/sdk/tsconfig.json | 5 +- generators/go/cmd/fern-go-fiber/main_test.go | 1 + generators/go/cmd/fern-go-model/main_test.go | 1 + generators/go/cmd/fern-go-sdk/main_test.go | 1 + generators/go/internal/generator/generator.go | 6 ++ .../go/internal/generator/v2/generator.go | 61 ++++++++++++++++++ generators/go/sdk/Dockerfile | 26 ++++++-- pnpm-lock.yaml | 24 +++++++ seed/go-sdk/seed.yml | 5 +- 32 files changed, 526 insertions(+), 31 deletions(-) create mode 100644 generators/go-v2/ast/src/ast/File.ts create mode 100644 generators/go-v2/base/src/cli/AbstractGoGeneratorCli.ts create mode 100644 generators/go-v2/base/src/cli/index.ts create mode 100644 generators/go-v2/base/src/project/GoFile.ts create mode 100644 generators/go-v2/sdk/src/SdkCustomConfig.ts create mode 100644 generators/go-v2/sdk/src/SdkGeneratorCli.ts create mode 100644 generators/go-v2/sdk/src/SdkGeneratorContext.ts create mode 100644 generators/go-v2/sdk/src/wiretest/WireTestGenerator.ts create mode 100644 generators/go/internal/generator/v2/generator.go diff --git a/generators/go-v2/ast/src/ast/File.ts b/generators/go-v2/ast/src/ast/File.ts new file mode 100644 index 00000000000..378d2d8ae42 --- /dev/null +++ b/generators/go-v2/ast/src/ast/File.ts @@ -0,0 +1,28 @@ +import { AstNode } from "./core/AstNode"; +import { Writer } from "./core/Writer"; + +export declare namespace File { + interface Args { + /* The list of nodes in the file. */ + nodes?: AstNode[]; + } +} + +export class File extends AstNode { + public readonly nodes: AstNode[]; + + constructor({ nodes }: File.Args = { nodes: [] }) { + super(); + this.nodes = nodes ?? []; + } + + public add(...nodes: AstNode[]): void { + this.nodes.push(...nodes); + } + + public write(writer: Writer): void { + for (const node of this.nodes) { + node.write(writer); + } + } +} diff --git a/generators/go-v2/ast/src/ast/Struct.ts b/generators/go-v2/ast/src/ast/Struct.ts index 5d10d57dd6a..3575ac2c581 100644 --- a/generators/go-v2/ast/src/ast/Struct.ts +++ b/generators/go-v2/ast/src/ast/Struct.ts @@ -41,7 +41,7 @@ export class Struct extends AstNode { public write(writer: Writer): void { writer.writeNode(new Comment({ docs: this.docs })); writer.write(`type ${this.name} struct {`); - if (this.fields.length > 0) { + if (this.fields.length === 0) { writer.writeLine("}"); } else { writer.newLine(); @@ -53,13 +53,13 @@ export class Struct extends AstNode { writer.dedent(); writer.writeLine("}"); } - if (this.constructor != null || this.methods.length > 0) { - writer.newLine(); - } - for (const method of this.methods) { - writer.writeNode(method); + + if (this.methods.length > 0) { writer.newLine(); + for (const method of this.methods) { + writer.writeNode(method); + writer.newLine(); + } } - return; } } diff --git a/generators/go-v2/ast/src/ast/__test__/Snippets.test.ts b/generators/go-v2/ast/src/ast/__test__/Snippets.test.ts index 7fc8625a384..4098dd7e4b7 100644 --- a/generators/go-v2/ast/src/ast/__test__/Snippets.test.ts +++ b/generators/go-v2/ast/src/ast/__test__/Snippets.test.ts @@ -1,4 +1,8 @@ +import { Field } from "../Field"; +import { File } from "../File"; +import { Func } from "../Func"; import { GoTypeReference } from "../GoTypeReference"; +import { Struct } from "../Struct"; import { Type } from "../Type"; import { TypeInstantiation } from "../TypeInstantiation"; import { AstNode } from "../core/AstNode"; @@ -281,6 +285,50 @@ describe("Snippets", () => { }); }); +describe("file", () => { + it("import collision", () => { + const file = new File(); + const foo = new Struct({ + name: "Foo", + importPath: "github.com/acme/acme-go" + }); + foo.addField( + new Field({ + name: "Name", + type: Type.reference( + new GoTypeReference({ + name: "Identifier", + importPath: "github.com/acme/acme-go/common" + }) + ) + }) + ); + const bar = new Struct({ + name: "Bar", + importPath: "github.com/acme/acme-go" + }); + bar.addField( + new Field({ + name: "Name", + type: Type.reference( + new GoTypeReference({ + name: "Identifier", + importPath: "github.com/acme/acme-go/nested/common" + }) + ) + }) + ); + file.add(foo, bar); + const content = file.toString({ + packageName: "example", + rootImportPath: "github.com/acme/acme-go", + importPath: "github.com/acme/consumer", + customConfig: {} + }); + expect(content).toMatchSnapshot(); + }); +}); + const USER_TYPE_REFERENCE = new GoTypeReference({ name: "User", importPath: "github.com/acme/acme-go" diff --git a/generators/go-v2/ast/src/ast/__test__/__snapshots__/Snippets.test.ts.snap b/generators/go-v2/ast/src/ast/__test__/__snapshots__/Snippets.test.ts.snap index df59f5e4292..7e378ddc23d 100644 --- a/generators/go-v2/ast/src/ast/__test__/__snapshots__/Snippets.test.ts.snap +++ b/generators/go-v2/ast/src/ast/__test__/__snapshots__/Snippets.test.ts.snap @@ -210,3 +210,20 @@ var value = acme.User{ Age: 42, }" `; + +exports[`file > import collision 1`] = ` +"package example + +import ( + common "github.com/acme/acme-go/common" + _common "github.com/acme/acme-go/nested/common" +) + +type Foo struct { + Name common.Identifier +} +type Bar struct { + Name _common.Identifier +} +" +`; diff --git a/generators/go-v2/ast/src/ast/core/Writer.ts b/generators/go-v2/ast/src/ast/core/Writer.ts index 27a666edf77..1a3485314b1 100644 --- a/generators/go-v2/ast/src/ast/core/Writer.ts +++ b/generators/go-v2/ast/src/ast/core/Writer.ts @@ -59,8 +59,9 @@ export class Writer extends AbstractWriter { if (maybeAlias != null) { return maybeAlias; } + const set = new Set(Object.values(this.imports)); let alias = this.getValidAlias(basename(importPath)); - while (alias in this.imports) { + while (set.has(alias)) { alias = "_" + alias; } this.imports[importPath] = alias; diff --git a/generators/go-v2/ast/src/ast/index.ts b/generators/go-v2/ast/src/ast/index.ts index 55546c1ca9c..0df86ad8434 100644 --- a/generators/go-v2/ast/src/ast/index.ts +++ b/generators/go-v2/ast/src/ast/index.ts @@ -2,6 +2,7 @@ export { CodeBlock } from "./CodeBlock"; export { Writer } from "./core/Writer"; export { Enum } from "./Enum"; export { Field } from "./Field"; +export { File } from "././File"; export { Func } from "./Func"; export { FuncInvocation } from "./FuncInvocation"; export { GoTypeReference } from "./GoTypeReference"; diff --git a/generators/go-v2/ast/src/context/AbstractGoGeneratorContext.ts b/generators/go-v2/ast/src/context/AbstractGoGeneratorContext.ts index 3f291f95050..c33204c3d2a 100644 --- a/generators/go-v2/ast/src/context/AbstractGoGeneratorContext.ts +++ b/generators/go-v2/ast/src/context/AbstractGoGeneratorContext.ts @@ -8,10 +8,12 @@ import { RelativeFilePath } from "@fern-api/path-utils"; import { FernFilepath, + HttpService, IntermediateRepresentation, Literal, Name, PrimitiveTypeV1, + ServiceId, Subpackage, SubpackageId, TypeDeclaration, @@ -50,6 +52,14 @@ export abstract class AbstractGoGeneratorContext< }); } + public getHttpServiceOrThrow(serviceId: ServiceId): HttpService { + const service = this.ir.services[serviceId]; + if (service == null) { + throw new Error(`Service with id ${serviceId} not found`); + } + return service; + } + public getSubpackageOrThrow(subpackageId: SubpackageId): Subpackage { const subpackage = this.ir.subpackages[subpackageId]; if (subpackage == null) { @@ -229,7 +239,10 @@ export abstract class AbstractGoGeneratorContext< return this.ir.types[typeId]; } - public abstract getLocationForTypeId(typeId: TypeId): FileLocation; + public getLocationForTypeId(typeId: TypeId): FileLocation { + const typeDeclaration = this.getTypeDeclarationOrThrow(typeId); + return this.getFileLocation(typeDeclaration.name.fernFilepath); + } protected getFileLocation(filepath: FernFilepath, suffix?: string): FileLocation { let parts = filepath.packagePath.map((path) => path.pascalCase.safeName.toLowerCase()); diff --git a/generators/go-v2/ast/src/go.ts b/generators/go-v2/ast/src/go.ts index dbfe6084df2..3db0dc6657c 100644 --- a/generators/go-v2/ast/src/go.ts +++ b/generators/go-v2/ast/src/go.ts @@ -2,6 +2,7 @@ import { CodeBlock, Enum, Field, + File, Func, FuncInvocation, GoTypeReference, @@ -23,6 +24,10 @@ export function field(args: Field.Args): Field { return new Field(args); } +export function file(args: File.Args = {}): File { + return new File(args); +} + export function func(args: Func.Args): Func { return new Func(args); } @@ -56,6 +61,7 @@ export { CodeBlock, Enum, Field, + File, Func, FuncInvocation, GoTypeReference as TypeReference, diff --git a/generators/go-v2/ast/src/index.ts b/generators/go-v2/ast/src/index.ts index f5c4113a0d8..62c9c482bed 100644 --- a/generators/go-v2/ast/src/index.ts +++ b/generators/go-v2/ast/src/index.ts @@ -2,3 +2,4 @@ export { AbstractGoGeneratorContext, type FileLocation } from "./context/Abstrac export { BaseGoCustomConfigSchema } from "./custom-config/BaseGoCustomConfigSchema"; export { resolveRootImportPath } from "./custom-config/resolveRootImportPath"; export * as go from "./go"; +export { GoFile } from "./ast/core/GoFile"; diff --git a/generators/go-v2/base/package.json b/generators/go-v2/base/package.json index b3aff31a9f2..74139f82268 100644 --- a/generators/go-v2/base/package.json +++ b/generators/go-v2/base/package.json @@ -32,6 +32,8 @@ "@fern-api/base-generator": "workspace:*", "@fern-api/fs-utils": "workspace:*", "@fern-api/go-ast": "workspace:*", + "@fern-api/logging-execa": "workspace:*", + "@fern-fern/ir-sdk": "^55.0.0", "@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.14", "esbuild": "^0.24.0", "tsup": "^8.0.2", diff --git a/generators/go-v2/base/src/cli/AbstractGoGeneratorCli.ts b/generators/go-v2/base/src/cli/AbstractGoGeneratorCli.ts new file mode 100644 index 00000000000..c4dd3a08115 --- /dev/null +++ b/generators/go-v2/base/src/cli/AbstractGoGeneratorCli.ts @@ -0,0 +1,24 @@ +import { AbstractGeneratorCli, parseIR } from "@fern-api/base-generator"; +import { AbsoluteFilePath } from "@fern-api/fs-utils"; +import { AbstractGoGeneratorContext } from "@fern-api/go-ast"; +import { BaseGoCustomConfigSchema } from "@fern-api/go-ast"; + +import { IntermediateRepresentation } from "@fern-fern/ir-sdk/api"; +import * as IrSerialization from "@fern-fern/ir-sdk/serialization"; + +export abstract class AbstractGoGeneratorCli< + CustomConfig extends BaseGoCustomConfigSchema, + GoGeneratorContext extends AbstractGoGeneratorContext +> extends AbstractGeneratorCli { + /** + * Parses the IR for the PHP generators + * @param irFilepath + * @returns + */ + protected async parseIntermediateRepresentation(irFilepath: string): Promise { + return await parseIR({ + absolutePathToIR: AbsoluteFilePath.of(irFilepath), + parse: IrSerialization.IntermediateRepresentation.parse + }); + } +} diff --git a/generators/go-v2/base/src/cli/index.ts b/generators/go-v2/base/src/cli/index.ts new file mode 100644 index 00000000000..76bc7b6acd8 --- /dev/null +++ b/generators/go-v2/base/src/cli/index.ts @@ -0,0 +1 @@ +export { AbstractGoGeneratorCli } from "./AbstractGoGeneratorCli"; diff --git a/generators/go-v2/base/src/index.ts b/generators/go-v2/base/src/index.ts index f01677eeca1..543e6cf2d85 100644 --- a/generators/go-v2/base/src/index.ts +++ b/generators/go-v2/base/src/index.ts @@ -1,6 +1,4 @@ -void runCli(); - -export async function runCli(): Promise { - // eslint-disable-next-line no-console - console.log("Noop..."); -} +export { AbstractGoGeneratorCli } from "./cli/AbstractGoGeneratorCli"; +export { GoFile } from "./project/GoFile"; +export { GoProject } from "./project/GoProject"; +export { FileGenerator } from "./FileGenerator"; diff --git a/generators/go-v2/base/src/project/GoFile.ts b/generators/go-v2/base/src/project/GoFile.ts new file mode 100644 index 00000000000..578056ae71e --- /dev/null +++ b/generators/go-v2/base/src/project/GoFile.ts @@ -0,0 +1,49 @@ +import { AbstractFormatter, File } from "@fern-api/base-generator"; +import { RelativeFilePath } from "@fern-api/fs-utils"; +import { BaseGoCustomConfigSchema, go } from "@fern-api/go-ast"; + +export declare namespace GoFile { + interface Args { + /* The node to be written to the Go source file */ + node: go.AstNode; + /* Directory of the file */ + directory: RelativeFilePath; + /* Filename of the file */ + filename: string; + /* The package name of the file */ + packageName: string; + /* The root import path of the module */ + rootImportPath: string; + /* The import path of the file */ + importPath: string; + /* Custom generator config */ + customConfig: BaseGoCustomConfigSchema; + /* Optional formatter */ + formatter?: AbstractFormatter; + } +} + +export class GoFile extends File { + constructor({ + node, + directory, + filename, + packageName, + rootImportPath, + importPath, + customConfig, + formatter + }: GoFile.Args) { + super( + filename, + directory, + node.toString({ + packageName, + rootImportPath, + importPath, + customConfig, + formatter + }) + ); + } +} diff --git a/generators/go-v2/base/src/project/GoProject.ts b/generators/go-v2/base/src/project/GoProject.ts index d787a15015d..c358317446e 100644 --- a/generators/go-v2/base/src/project/GoProject.ts +++ b/generators/go-v2/base/src/project/GoProject.ts @@ -1,2 +1,56 @@ -// eslint-disable-next-line @typescript-eslint/no-extraneous-class -export class GoProject {} +import { mkdir } from "fs/promises"; + +import { AbstractProject, File } from "@fern-api/base-generator"; +import { AbsoluteFilePath } from "@fern-api/fs-utils"; +import { AbstractGoGeneratorContext, BaseGoCustomConfigSchema } from "@fern-api/go-ast"; +import { loggingExeca } from "@fern-api/logging-execa"; + +/** + * In memory representation of a Go project. + */ +export class GoProject extends AbstractProject> { + private sourceFiles: File[] = []; + + public constructor({ context }: { context: AbstractGoGeneratorContext }) { + super(context); + } + + public addGoFiles(file: File): void { + this.sourceFiles.push(file); + } + + public async persist(): Promise { + await this.writeGoFiles({ + absolutePathToDirectory: this.absolutePathToOutputDirectory, + files: this.sourceFiles + }); + } + + private async writeGoFiles({ + absolutePathToDirectory, + files + }: { + absolutePathToDirectory: AbsoluteFilePath; + files: File[]; + }): Promise { + await this.mkdir(absolutePathToDirectory); + await Promise.all(files.map(async (file) => await file.write(absolutePathToDirectory))); + if (files.length > 0) { + // TODO: Uncomment this once the go-v2 generator is responsible for producing the go.mod file. + // Otherwise, we get a "directory prefix . does not contain main module or its selected dependencies" error. + // + // --- + // + // await loggingExeca(this.context.logger, "go", ["fmt", "./..."], { + // doNotPipeOutput: true, + // cwd: absolutePathToDirectory + // }); + } + return absolutePathToDirectory; + } + + private async mkdir(absolutePathToDirectory: AbsoluteFilePath): Promise { + this.context.logger.debug(`mkdir ${absolutePathToDirectory}`); + await mkdir(absolutePathToDirectory, { recursive: true }); + } +} diff --git a/generators/go-v2/base/src/project/index.ts b/generators/go-v2/base/src/project/index.ts index 5735f417b27..fcb46bda6da 100644 --- a/generators/go-v2/base/src/project/index.ts +++ b/generators/go-v2/base/src/project/index.ts @@ -1 +1,2 @@ +export { GoFile } from "./GoFile"; export { GoProject } from "./GoProject"; diff --git a/generators/go-v2/base/tsconfig.json b/generators/go-v2/base/tsconfig.json index 5729dd9ecb2..48ab6067069 100644 --- a/generators/go-v2/base/tsconfig.json +++ b/generators/go-v2/base/tsconfig.json @@ -5,6 +5,7 @@ "references": [ { "path": "../../../packages/commons/core-utils" }, { "path": "../../../packages/commons/fs-utils" }, + { "path": "../../../packages/commons/logging-execa" }, { "path": "../../base" }, { "path": "../ast" } ] diff --git a/generators/go-v2/sdk/package.json b/generators/go-v2/sdk/package.json index 3f73dd0f8c2..b7e133702ba 100644 --- a/generators/go-v2/sdk/package.json +++ b/generators/go-v2/sdk/package.json @@ -29,15 +29,21 @@ "dockerTagLatest": "pnpm dist:cli && docker build -f ./Dockerfile -t fernapi/fern-go-sdk:latest ../../.." }, "devDependencies": { + "@fern-api/go-ast": "workspace:*", + "@fern-api/go-base": "workspace:*", + "@fern-api/base-generator": "workspace:*", + "@fern-api/configs": "workspace:*", + "@fern-api/path-utils": "workspace:*", + "@fern-fern/generator-exec-sdk": "^0.0.898", + "@fern-fern/ir-sdk": "^55.0.0", + "@trivago/prettier-plugin-sort-imports": "^5.2.1", + "@types/node": "18.15.3", "@yarnpkg/esbuild-plugin-pnp": "^3.0.0-rc.14", "esbuild": "^0.24.0", "tsup": "^8.0.2", - "@fern-api/configs": "workspace:*", - "@types/node": "18.15.3", "depcheck": "^1.4.7", "eslint": "^8.56.0", "prettier": "^3.4.2", - "@trivago/prettier-plugin-sort-imports": "^5.2.1", "typescript": "5.7.2", "vitest": "^2.1.8" } diff --git a/generators/go-v2/sdk/src/SdkCustomConfig.ts b/generators/go-v2/sdk/src/SdkCustomConfig.ts new file mode 100644 index 00000000000..efc1d5865ad --- /dev/null +++ b/generators/go-v2/sdk/src/SdkCustomConfig.ts @@ -0,0 +1,5 @@ +import { BaseGoCustomConfigSchema } from "@fern-api/go-ast"; + +export const SdkCustomConfigSchema: typeof BaseGoCustomConfigSchema = BaseGoCustomConfigSchema; + +export type SdkCustomConfigSchema = BaseGoCustomConfigSchema; diff --git a/generators/go-v2/sdk/src/SdkGeneratorCli.ts b/generators/go-v2/sdk/src/SdkGeneratorCli.ts new file mode 100644 index 00000000000..d07bbbe11c3 --- /dev/null +++ b/generators/go-v2/sdk/src/SdkGeneratorCli.ts @@ -0,0 +1,64 @@ +import { GeneratorNotificationService } from "@fern-api/base-generator"; +import { go } from "@fern-api/go-ast"; +import { AbstractGoGeneratorCli } from "@fern-api/go-base"; + +import { FernGeneratorExec } from "@fern-fern/generator-exec-sdk"; +import { IntermediateRepresentation } from "@fern-fern/ir-sdk/api"; + +import { SdkCustomConfigSchema } from "./SdkCustomConfig"; +import { SdkGeneratorContext } from "./SdkGeneratorContext"; +import { WireTestGenerator } from "./wiretest/WireTestGenerator"; + +export class SdkGeneratorCLI extends AbstractGoGeneratorCli { + protected constructContext({ + ir, + customConfig, + generatorConfig, + generatorNotificationService + }: { + ir: IntermediateRepresentation; + customConfig: SdkCustomConfigSchema; + generatorConfig: FernGeneratorExec.GeneratorConfig; + generatorNotificationService: GeneratorNotificationService; + }): SdkGeneratorContext { + return new SdkGeneratorContext(ir, generatorConfig, customConfig, generatorNotificationService); + } + + protected parseCustomConfigOrThrow(customConfig: unknown): SdkCustomConfigSchema { + const parsed = customConfig != null ? SdkCustomConfigSchema.parse(customConfig) : undefined; + if (parsed != null) { + return parsed; + } + return {}; + } + + protected async publishPackage(context: SdkGeneratorContext): Promise { + throw new Error("Method not implemented."); + } + + protected async writeForGithub(context: SdkGeneratorContext): Promise { + await this.generate(context); + } + + protected async writeForDownload(context: SdkGeneratorContext): Promise { + await this.generate(context); + } + + protected async generate(context: SdkGeneratorContext): Promise { + // TODO: Enable wire tests, when available. + // this.generateWireTests(context); + await context.project.persist(); + } + + private generateWireTests(context: SdkGeneratorContext) { + const wireTestGenerator = new WireTestGenerator(context); + for (const subpackage of Object.values(context.ir.subpackages)) { + const serviceId = subpackage.service != null ? subpackage.service : undefined; + if (serviceId == null) { + continue; + } + const service = context.getHttpServiceOrThrow(serviceId); + context.project.addGoFiles(wireTestGenerator.generate({ serviceId, endpoints: service.endpoints })); + } + } +} diff --git a/generators/go-v2/sdk/src/SdkGeneratorContext.ts b/generators/go-v2/sdk/src/SdkGeneratorContext.ts new file mode 100644 index 00000000000..92178f4e28b --- /dev/null +++ b/generators/go-v2/sdk/src/SdkGeneratorContext.ts @@ -0,0 +1,22 @@ +import { GeneratorNotificationService } from "@fern-api/base-generator"; +import { AbstractGoGeneratorContext, FileLocation } from "@fern-api/go-ast"; +import { GoProject } from "@fern-api/go-base"; + +import { FernGeneratorExec } from "@fern-fern/generator-exec-sdk"; +import { IntermediateRepresentation, TypeId } from "@fern-fern/ir-sdk/api"; + +import { SdkCustomConfigSchema } from "./SdkCustomConfig"; + +export class SdkGeneratorContext extends AbstractGoGeneratorContext { + public readonly project: GoProject; + + public constructor( + public readonly ir: IntermediateRepresentation, + public readonly config: FernGeneratorExec.config.GeneratorConfig, + public readonly customConfig: SdkCustomConfigSchema, + public readonly generatorNotificationService: GeneratorNotificationService + ) { + super(ir, config, customConfig, generatorNotificationService); + this.project = new GoProject({ context: this }); + } +} diff --git a/generators/go-v2/sdk/src/cli.ts b/generators/go-v2/sdk/src/cli.ts index a365ecd4f45..4792f0e1644 100644 --- a/generators/go-v2/sdk/src/cli.ts +++ b/generators/go-v2/sdk/src/cli.ts @@ -1,6 +1,8 @@ +import { SdkGeneratorCLI } from "./SdkGeneratorCli"; + void runCli(); export async function runCli(): Promise { - // eslint-disable-next-line no-console - console.log("no-op"); + const cli = new SdkGeneratorCLI(); + await cli.run(); } diff --git a/generators/go-v2/sdk/src/wiretest/WireTestGenerator.ts b/generators/go-v2/sdk/src/wiretest/WireTestGenerator.ts new file mode 100644 index 00000000000..d5cc08409bf --- /dev/null +++ b/generators/go-v2/sdk/src/wiretest/WireTestGenerator.ts @@ -0,0 +1,39 @@ +import { go } from "@fern-api/go-ast"; +import { GoFile } from "@fern-api/go-base"; +import { RelativeFilePath } from "@fern-api/path-utils"; + +import { HttpEndpoint, ServiceId } from "@fern-fern/ir-sdk/api"; + +import { SdkGeneratorContext } from "../SdkGeneratorContext"; + +export class WireTestGenerator { + private readonly context: SdkGeneratorContext; + + public constructor(context: SdkGeneratorContext) { + this.context = context; + } + + public generate({ serviceId, endpoints }: { serviceId: ServiceId; endpoints: HttpEndpoint[] }): GoFile { + // TODO: Filter out all of the non-JSON endpoints. + // TODO: Map the endpoint's full path to the dynamic IR representation (e.g. POST /users). + // TODO: Map the example into the dynamic IR payload (similar to the test suite generator). + // TODO: Include every test case as a separate item in the table-driven tests. + const file = go.file(); + file.add( + go.func({ + name: "nop", + parameters: [], + return_: [] + }) + ); + return new GoFile({ + node: file, + directory: RelativeFilePath.of("user"), + filename: "user_test.go", + packageName: "user_test", + rootImportPath: "github.com/fern-api/fern-go", + importPath: "github.com/fern-api/fern-go", + customConfig: this.context.customConfig + }); + } +} diff --git a/generators/go-v2/sdk/tsconfig.json b/generators/go-v2/sdk/tsconfig.json index 5729dd9ecb2..c57ff765b69 100644 --- a/generators/go-v2/sdk/tsconfig.json +++ b/generators/go-v2/sdk/tsconfig.json @@ -4,8 +4,9 @@ "include": ["./src/**/*"], "references": [ { "path": "../../../packages/commons/core-utils" }, - { "path": "../../../packages/commons/fs-utils" }, + { "path": "../../../packages/commons/path-utils" }, { "path": "../../base" }, - { "path": "../ast" } + { "path": "../ast" }, + { "path": "../base" } ] } diff --git a/generators/go/cmd/fern-go-fiber/main_test.go b/generators/go/cmd/fern-go-fiber/main_test.go index be8b4896e9a..039192cdaea 100644 --- a/generators/go/cmd/fern-go-fiber/main_test.go +++ b/generators/go/cmd/fern-go-fiber/main_test.go @@ -14,5 +14,6 @@ const ( ) func TestFixtures(t *testing.T) { + t.Skip("These tests require running in a Docker container with /bin/go-v2 installed") cmdtest.TestFixtures(t, commandName, testdataPath, usage, run) } diff --git a/generators/go/cmd/fern-go-model/main_test.go b/generators/go/cmd/fern-go-model/main_test.go index 80d6af35b78..083fbe62c94 100644 --- a/generators/go/cmd/fern-go-model/main_test.go +++ b/generators/go/cmd/fern-go-model/main_test.go @@ -24,6 +24,7 @@ const ( ) func TestFixtures(t *testing.T) { + t.Skip("These tests require running in a Docker container with /bin/go-v2 installed") cmdtest.TestFixtures(t, commandName, testdataPath, usage, run) } diff --git a/generators/go/cmd/fern-go-sdk/main_test.go b/generators/go/cmd/fern-go-sdk/main_test.go index 3c7655ccd62..49952ddd5db 100644 --- a/generators/go/cmd/fern-go-sdk/main_test.go +++ b/generators/go/cmd/fern-go-sdk/main_test.go @@ -20,6 +20,7 @@ const ( ) func TestFixtures(t *testing.T) { + t.Skip("These tests require running in a Docker container with /bin/go-v2 installed") cmdtest.TestFixtures(t, commandName, testdataPath, usage, run) } diff --git a/generators/go/internal/generator/generator.go b/generators/go/internal/generator/generator.go index c66525ea8dd..8be77eda264 100644 --- a/generators/go/internal/generator/generator.go +++ b/generators/go/internal/generator/generator.go @@ -13,6 +13,7 @@ import ( "github.com/fern-api/fern-go/internal/coordinator" "github.com/fern-api/fern-go/internal/fern/ir" fernir "github.com/fern-api/fern-go/internal/fern/ir" + gov2 "github.com/fern-api/fern-go/internal/generator/v2" generatorexec "github.com/fern-api/generator-exec-go" ) @@ -358,6 +359,11 @@ func (g *Generator) generate(ir *fernir.IntermediateRepresentation, mode Mode) ( case ModeFiber: break case ModeClient: + // If we're running in SDK mode, start by running the go-v2 SDK generator. + v2 := gov2.New(g.coordinator) + if err := v2.Run(); err != nil { + return nil, err + } var ( generatedAuth *GeneratedAuth generatedEnvironment *GeneratedEnvironment diff --git a/generators/go/internal/generator/v2/generator.go b/generators/go/internal/generator/v2/generator.go new file mode 100644 index 00000000000..399a482ccc9 --- /dev/null +++ b/generators/go/internal/generator/v2/generator.go @@ -0,0 +1,61 @@ +package v2 + +import ( + "bytes" + "errors" + "fmt" + "os" + "os/exec" + + "github.com/fern-api/fern-go/internal/coordinator" + generatorexec "github.com/fern-api/generator-exec-go" +) + +// v2BinPath is the path to the go-v2 binary included in the SDK +// generator docker image. +const v2BinPath = "/bin/go-v2" + +// Generator represents a shim used to go-v2 SDK generator. +type Generator struct { + coordinator *coordinator.Client +} + +// New returns a new *Generator. +func New(coordinator *coordinator.Client) *Generator { + return &Generator{ + coordinator: coordinator, + } +} + +// Run runs the go-v2 SDK generator. +func (g *Generator) Run() error { + if len(os.Args) < 2 { + return errors.New("internal error; failed to resolve configuration file path") + } + + if _, err := os.Stat(v2BinPath); os.IsNotExist(err) { + return fmt.Errorf("go-v2 binary not found at %s", v2BinPath) + } + + configFilepath := os.Args[1] + if _, err := os.Stat(configFilepath); os.IsNotExist(err) { + return fmt.Errorf("configuration file not found at %s", configFilepath) + } + + g.coordinator.Log( + generatorexec.LogLevelDebug, + "Running go-v2 SDK generator...", + ) + + stderr := bytes.NewBuffer(nil) + cmd := exec.Command("node", v2BinPath, configFilepath) + cmd.Stderr = stderr + if err := cmd.Run(); err != nil { + return errors.New(stderr.String()) + } + + return g.coordinator.Log( + generatorexec.LogLevelDebug, + "Successfully ran go-v2 generator", + ) +} diff --git a/generators/go/sdk/Dockerfile b/generators/go/sdk/Dockerfile index fa30b52a7db..25f4ce8493e 100644 --- a/generators/go/sdk/Dockerfile +++ b/generators/go/sdk/Dockerfile @@ -1,16 +1,32 @@ +# Stage 1: Build Node CLI +FROM node:20.18-alpine3.20 AS node + +RUN apk --no-cache add git zip \ + && git config --global user.name "fern" \ + && git config --global user.email "hey@buildwithfern.com" + +COPY generators/go-v2/sdk/dist/cli.cjs /dist/cli.cjs + +# Stage 2: Final Go image FROM golang:1.22.7-alpine3.19 WORKDIR /workspace -RUN apk add --no-cache ca-certificates git +RUN apk add --no-cache ca-certificates git nodejs -COPY go.mod go.sum /workspace/ +COPY generators/go/go.mod generators/go/go.sum /workspace/ RUN go mod download -COPY cmd /workspace/cmd -COPY internal /workspace/internal -COPY version.go /workspace/version.go +COPY generators/go/cmd /workspace/cmd +COPY generators/go/internal /workspace/internal +COPY generators/go/version.go /workspace/version.go + +# Copy Node CLI from first stage and rename it /bin/go-v2 +COPY --from=node /dist/cli.cjs /bin/go-v2 +RUN chmod +x /bin/go-v2 RUN CGO_ENABLED=0 go build -ldflags "-s -w" -trimpath -buildvcs=false -o /fern-go-sdk ./cmd/fern-go-sdk +RUN test -f /bin/go-v2 || echo "go-v2 CLI not found or not executable" + ENTRYPOINT ["/fern-go-sdk"] diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 47dcba6bc42..45407b1ebae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -505,6 +505,12 @@ importers: '@fern-api/go-ast': specifier: workspace:* version: link:../ast + '@fern-api/logging-execa': + specifier: workspace:* + version: link:../../../packages/commons/logging-execa + '@fern-fern/ir-sdk': + specifier: ^55.0.0 + version: 55.0.0 '@trivago/prettier-plugin-sort-imports': specifier: ^5.2.1 version: 5.2.1(@vue/compiler-sfc@3.5.13)(prettier@3.4.2) @@ -671,9 +677,27 @@ importers: generators/go-v2/sdk: devDependencies: + '@fern-api/base-generator': + specifier: workspace:* + version: link:../../base '@fern-api/configs': specifier: workspace:* version: link:../../../packages/configs + '@fern-api/go-ast': + specifier: workspace:* + version: link:../ast + '@fern-api/go-base': + specifier: workspace:* + version: link:../base + '@fern-api/path-utils': + specifier: workspace:* + version: link:../../../packages/commons/path-utils + '@fern-fern/generator-exec-sdk': + specifier: ^0.0.898 + version: 0.0.898 + '@fern-fern/ir-sdk': + specifier: ^55.0.0 + version: 55.0.0 '@trivago/prettier-plugin-sort-imports': specifier: ^5.2.1 version: 5.2.1(@vue/compiler-sfc@3.5.13)(prettier@3.4.2) diff --git a/seed/go-sdk/seed.yml b/seed/go-sdk/seed.yml index 764269ea62f..bad0aca09a6 100644 --- a/seed/go-sdk/seed.yml +++ b/seed/go-sdk/seed.yml @@ -15,14 +15,15 @@ publish: workingDirectory: generators/go preBuildCommands: - go build ./... + - pnpm --filter @fern-api/go-sdk dist:cli docker: file: ./generators/go/sdk/Dockerfile image: fernapi/fern-go-sdk - context: ./generators/go + context: . test: docker: image: fernapi/fern-go-sdk:latest - command: docker build -f ./generators/go/sdk/Dockerfile -t fernapi/fern-go-sdk:latest ./generators/go + command: pnpm --filter @fern-api/go-sdk dist:cli && docker build -f ./generators/go/sdk/Dockerfile -t fernapi/fern-go-sdk:latest . language: go generatorType: SDK defaultOutputMode: github