Skip to content

Commit

Permalink
feat(cli): Support protoc-gen-openapi source root (#6003)
Browse files Browse the repository at this point in the history
  • Loading branch information
amckinney authored Feb 15, 2025
1 parent 4c1ff6f commit f3cac44
Show file tree
Hide file tree
Showing 19 changed files with 83 additions and 152 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ jobs:
go-version: "stable"

- name: Install protoc-gen-openapi
run: go install github.com/google/gnostic/cmd/protoc-gen-openapi@v0.7.0
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest

- name: Run tests
run: pnpm test
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-go-dynamic-snippets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
go-version: "stable"

- name: Install protoc-gen-openapi
run: go install github.com/google/gnostic/cmd/protoc-gen-openapi@v0.7.0
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest

- name: 🧪 Build
run: pnpm --filter=${{ env.PACKAGE_NAME }} compile
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-php-dynamic-snippets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
go-version: "stable"

- name: Install protoc-gen-openapi
run: go install github.com/google/gnostic/cmd/protoc-gen-openapi@v0.7.0
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest

- name: 🧪 Build
run: pnpm --filter=${{ env.PACKAGE_NAME }} compile
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-snippets-core.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
go-version: "stable"

- name: Install protoc-gen-openapi
run: go install github.com/google/gnostic/cmd/protoc-gen-openapi@v0.7.0
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest

- name: 🧪 Build
run: pnpm --filter=${{ env.PACKAGE_NAME }} compile
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish-ts-dynamic-snippets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
go-version: "stable"

- name: Install protoc-gen-openapi
run: go install github.com/google/gnostic/cmd/protoc-gen-openapi@v0.7.0
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest

- name: 🧪 Build
run: pnpm --filter=${{ env.PACKAGE_NAME }} compile
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-definitions.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ jobs:
go-version: "stable"

- name: Install protoc-gen-openapi
run: go install github.com/google/gnostic/cmd/protoc-gen-openapi@v0.7.0
run: go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest

- name: Fern check
env:
Expand Down
5 changes: 5 additions & 0 deletions fern/pages/changelogs/cli/2025-02-15.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
## 0.53.2
**`(fix):`** The OpenAPI parser now prefers the `source` extension set in the OpenAPI spec, and only writes
it if it is not already set.


Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export const FERN_TYPE_EXTENSIONS: Plugin = {
};

export const FernOpenAPIExtension = {
SOURCE: TypedExtensionId.of<string>("source"),

SDK_METHOD_NAME: TypedExtensionId.of<string>("x-fern-sdk-method-name"),
SDK_GROUP_NAME: TypedExtensionId.of<string | string[]>("x-fern-sdk-group-name"),

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import { OpenAPIV3 } from "openapi-types";

import { Source } from "@fern-api/openapi-ir";

import { getExtension } from "../../../getExtension";
import { FernOpenAPIExtension } from "./fernExtensions";

export function getSourceExtension(schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject): Source | undefined {
const sourceFilepath = getExtension<string>(schema, FernOpenAPIExtension.SOURCE);
if (sourceFilepath == null) {
return undefined;
}
if (sourceFilepath.endsWith(".proto")) {
return Source.protobuf({ file: sourceFilepath });
}
return Source.openapi({ file: sourceFilepath });
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { getExamples } from "../openapi/v3/extensions/getExamples";
import { getFernEncoding } from "../openapi/v3/extensions/getFernEncoding";
import { getFernEnum } from "../openapi/v3/extensions/getFernEnum";
import { getFernTypeExtension } from "../openapi/v3/extensions/getFernTypeExtension";
import { getSourceExtension } from "../openapi/v3/extensions/getSourceExtension";
import { getValueIfBoolean } from "../utils/getValue";
import { SchemaParserContext } from "./SchemaParserContext";
import { convertAdditionalProperties, wrapMap } from "./convertAdditionalProperties";
Expand Down Expand Up @@ -52,12 +53,13 @@ export function convertSchema(
wrapAsNullable: boolean,
context: SchemaParserContext,
breadcrumbs: string[],
source: Source,
fileSource: Source,
namespace: string | undefined,
referencedAsRequest = false,
propertiesToExclude: Set<string> = new Set(),
fallback?: string | number | boolean | unknown[]
): SchemaWithExample {
const source = getSourceExtension(schema) ?? fileSource;
const encoding = getEncoding({ schema, logger: context.logger });
if (isReferenceObject(schema)) {
const schemaId = getSchemaIdFromReference(schema);
Expand Down
8 changes: 8 additions & 0 deletions packages/cli/cli/versions.yml
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
- changelogEntry:
- summary: |
The OpenAPI parser now prefers the `source` extension set in the OpenAPI spec, and only writes
it if it is not already set.
type: fix
irVersion: 55
version: 0.53.2

- changelogEntry:
- summary: |
The OpenAPI v2 parser now parses Server specifications with variables.
Expand Down
3 changes: 2 additions & 1 deletion packages/cli/workspace/commons/src/Spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ParseOpenAPIOptions } from "@fern-api/openapi-ir-parser";
import { AbsoluteFilePath } from "@fern-api/path-utils";
import { AbsoluteFilePath, RelativeFilePath } from "@fern-api/path-utils";

import { Source } from "./Source";

Expand All @@ -19,6 +19,7 @@ export interface ProtobufSpec {
absoluteFilepathToProtobufRoot: AbsoluteFilePath;
absoluteFilepathToProtobufTarget: AbsoluteFilePath;
absoluteFilepathToOverrides: AbsoluteFilePath | undefined;
relativeFilepathToProtobufRoot: RelativeFilePath;
generateLocally: boolean;
settings?: ParseOpenAPIOptions;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,6 @@ import { TaskContext } from "@fern-api/task-context";
const PROTOBUF_GENERATOR_CONFIG_FILENAME = "buf.gen.yaml";
const PROTOBUF_GENERATOR_OUTPUT_PATH = "output";
const PROTOBUF_GENERATOR_OUTPUT_FILEPATH = `${PROTOBUF_GENERATOR_OUTPUT_PATH}/openapi.yaml`;
const PROTOBUF_GENERATOR_CONFIG = `
version: v1
plugins:
- plugin: openapi
out: ${PROTOBUF_GENERATOR_OUTPUT_PATH}
opt:
- title=""
- enum_type=string
- default_response=false
`;

export class ProtobufOpenAPIGenerator {
private context: TaskContext;
Expand All @@ -29,27 +19,36 @@ export class ProtobufOpenAPIGenerator {
public async generate({
absoluteFilepathToProtobufRoot,
absoluteFilepathToProtobufTarget,
relativeFilepathToProtobufRoot,
local
}: {
absoluteFilepathToProtobufRoot: AbsoluteFilePath;
absoluteFilepathToProtobufTarget: AbsoluteFilePath;
relativeFilepathToProtobufRoot: RelativeFilePath;
local: boolean;
}): Promise<AbsoluteFilePath> {
if (local) {
return this.generateLocal({ absoluteFilepathToProtobufRoot, absoluteFilepathToProtobufTarget });
return this.generateLocal({
absoluteFilepathToProtobufRoot,
absoluteFilepathToProtobufTarget,
relativeFilepathToProtobufRoot
});
}
return this.generateRemote();
}

private async generateLocal({
absoluteFilepathToProtobufRoot,
absoluteFilepathToProtobufTarget
absoluteFilepathToProtobufTarget,
relativeFilepathToProtobufRoot
}: {
absoluteFilepathToProtobufRoot: AbsoluteFilePath;
absoluteFilepathToProtobufTarget: AbsoluteFilePath;
relativeFilepathToProtobufRoot: RelativeFilePath;
}): Promise<AbsoluteFilePath> {
const protobufGeneratorConfigPath = await this.setupProtobufGeneratorConfig({
absoluteFilepathToProtobufRoot
absoluteFilepathToProtobufRoot,
relativeFilepathToProtobufRoot
});
const protoTargetRelativeFilePath = relative(absoluteFilepathToProtobufRoot, absoluteFilepathToProtobufTarget);
return this.doGenerateLocal({
Expand All @@ -59,15 +58,17 @@ export class ProtobufOpenAPIGenerator {
}

private async setupProtobufGeneratorConfig({
absoluteFilepathToProtobufRoot
absoluteFilepathToProtobufRoot,
relativeFilepathToProtobufRoot
}: {
absoluteFilepathToProtobufRoot: AbsoluteFilePath;
relativeFilepathToProtobufRoot: RelativeFilePath;
}): Promise<AbsoluteFilePath> {
const protobufGeneratorConfigPath = AbsoluteFilePath.of((await tmp.dir()).path);
await cp(absoluteFilepathToProtobufRoot, protobufGeneratorConfigPath, { recursive: true });
await writeFile(
join(protobufGeneratorConfigPath, RelativeFilePath.of(PROTOBUF_GENERATOR_CONFIG_FILENAME)),
PROTOBUF_GENERATOR_CONFIG
getProtobufGeneratorConfig({ relativeFilepathToProtobufRoot })
);
return protobufGeneratorConfigPath;
}
Expand Down Expand Up @@ -96,7 +97,7 @@ export class ProtobufOpenAPIGenerator {
await which(["protoc-gen-openapi"]);
} catch (err) {
this.context.failAndThrow(
"Missing required dependency; please install 'protoc-gen-openapi' to continue (e.g. 'brew install go && go install github.com/google/gnostic/cmd/protoc-gen-openapi@latest')."
"Missing required dependency; please install 'protoc-gen-openapi' to continue (e.g. 'brew install go && go install github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi@latest')."
);
}

Expand All @@ -113,3 +114,21 @@ export class ProtobufOpenAPIGenerator {
this.context.failAndThrow("Remote Protobuf generation is unimplemented.");
}
}

function getProtobufGeneratorConfig({
relativeFilepathToProtobufRoot
}: {
relativeFilepathToProtobufRoot: RelativeFilePath;
}): string {
return `
version: v1
plugins:
- plugin: openapi
out: ${PROTOBUF_GENERATOR_OUTPUT_PATH}
opt:
- title=""
- enum_type=string
- default_response=false
- source_root=${relativeFilepathToProtobufRoot}
`;
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export async function convertProtobufToOpenAPI({
const openAPIAbsoluteFilePath = await generator.generate({
absoluteFilepathToProtobufRoot: protobufSpec.absoluteFilepathToProtobufRoot,
absoluteFilepathToProtobufTarget: protobufSpec.absoluteFilepathToProtobufTarget,
relativeFilepathToProtobufRoot: protobufSpec.relativeFilepathToProtobufRoot,
local: protobufSpec.generateLocally
});
return {
Expand Down
2 changes: 0 additions & 2 deletions packages/cli/workspace/loader/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,11 +35,9 @@
"@fern-api/fs-utils": "workspace:*",
"@fern-api/lazy-fern-workspace": "workspace:*",
"@fern-api/logger": "workspace:*",
"@fern-api/logging-execa": "workspace:*",
"@fern-api/task-context": "workspace:*",
"chalk": "^5.3.0",
"js-yaml": "^4.1.0",
"tmp-promise": "^3.0.3",
"zod": "^3.22.3"
},
"devDependencies": {
Expand Down
7 changes: 3 additions & 4 deletions packages/cli/workspace/loader/src/loadAPIWorkspace.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,8 @@ export async function loadSingleNamespaceAPIWorkspace({
? join(absolutePathToWorkspace, RelativeFilePath.of(definition.overrides))
: undefined;
if (definition.schema.type === "protobuf") {
const absoluteFilepathToProtobufRoot = join(
absolutePathToWorkspace,
RelativeFilePath.of(definition.schema.root)
);
const relativeFilepathToProtobufRoot = RelativeFilePath.of(definition.schema.root);
const absoluteFilepathToProtobufRoot = join(absolutePathToWorkspace, relativeFilepathToProtobufRoot);
if (!(await doesPathExist(absoluteFilepathToProtobufRoot))) {
return {
didSucceed: false,
Expand Down Expand Up @@ -69,6 +67,7 @@ export async function loadSingleNamespaceAPIWorkspace({
absoluteFilepathToProtobufRoot,
absoluteFilepathToProtobufTarget,
absoluteFilepathToOverrides,
relativeFilepathToProtobufRoot,
generateLocally: definition.schema.localGeneration,
settings: {
audiences: definition.audiences ?? [],
Expand Down
Loading

0 comments on commit f3cac44

Please sign in to comment.