diff --git a/cmd/protoc-gen-openapi/examples/tests/nogopackage/message.proto b/cmd/protoc-gen-openapi/examples/tests/nogopackage/message.proto new file mode 100644 index 00000000..d5c65c9c --- /dev/null +++ b/cmd/protoc-gen-openapi/examples/tests/nogopackage/message.proto @@ -0,0 +1,37 @@ +// Copyright 2021 Google LLC. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +syntax = "proto3"; + +package tests.nogopackage.message.v1; + +import "google/api/annotations.proto"; + +service Messaging { + rpc GetMessage(GetMessageRequest) returns(Message) { + option(google.api.http) = { + get: "/v1/messages/{message_id}" + }; + } +} + +message GetMessageRequest { + string message_id = 1; +} + +message Message { + string message_id = 1; + string text = 2; +} diff --git a/cmd/protoc-gen-openapi/examples/tests/nogopackage/openapi.yaml b/cmd/protoc-gen-openapi/examples/tests/nogopackage/openapi.yaml new file mode 100644 index 00000000..634faf18 --- /dev/null +++ b/cmd/protoc-gen-openapi/examples/tests/nogopackage/openapi.yaml @@ -0,0 +1,84 @@ +# Generated with protoc-gen-openapi +# https://github.com/fern-api/protoc-gen-openapi/tree/master/cmd/protoc-gen-openapi + +openapi: 3.0.3 +info: + title: Messaging API + version: 0.0.1 +paths: + /v1/messages/{message_id}: + get: + tags: + - Messaging + operationId: Messaging_GetMessage + parameters: + - name: message_id + in: path + required: true + schema: + type: string + x-fern-encoding: + proto: + type: google.protobuf.StringValue + responses: + "200": + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/Message' + default: + description: Default error response + content: + application/json: + schema: + $ref: '#/components/schemas/GoogleRpcStatus' + x-fern-sdk-group-name: Messaging + x-fern-sdk-method-name: GetMessage + x-fern-request-name: GetMessageRequest +components: + schemas: + GoogleProtobufAny: + type: object + properties: + '@type': + type: string + description: The type of the serialized message. + additionalProperties: true + description: Contains an arbitrary serialized message along with a @type that describes the type of the serialized message. + x-fern-encoding: + proto: + type: google.protobuf.Any + GoogleRpcStatus: + type: object + properties: + code: + type: integer + description: The status code, which should be an enum value of [google.rpc.Code][google.rpc.Code]. + format: int32 + message: + type: string + description: A developer-facing error message, which should be in English. Any user-facing error message should be localized and sent in the [google.rpc.Status.details][google.rpc.Status.details] field, or localized by the client. + details: + type: array + items: + $ref: '#/components/schemas/GoogleProtobufAny' + description: A list of messages that carry the error details. There is a common set of message types for APIs to use. + description: 'The `Status` type defines a logical error model that is suitable for different programming environments, including REST APIs and RPC APIs. It is used by [gRPC](https://github.com/grpc). Each `Status` message contains three pieces of data: error code, error message, and error details. You can find out more about this error model and how to work with it in the [API Design Guide](https://cloud.google.com/apis/design/errors).' + Message: + type: object + properties: + message_id: + type: string + x-fern-encoding: + proto: + type: google.protobuf.StringValue + text: + type: string + x-fern-encoding: + proto: + type: google.protobuf.StringValue + x-fern-type-name: Message + source: tests/nogopackage/message.proto +tags: + - name: Messaging diff --git a/cmd/protoc-gen-openapi/main.go b/cmd/protoc-gen-openapi/main.go index e42dca91..60fc9c51 100644 --- a/cmd/protoc-gen-openapi/main.go +++ b/cmd/protoc-gen-openapi/main.go @@ -17,11 +17,16 @@ package main import ( "flag" + "fmt" + "io" + "os" "path/filepath" "strings" "github.com/fern-api/protoc-gen-openapi/cmd/protoc-gen-openapi/generator" "google.golang.org/protobuf/compiler/protogen" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/descriptorpb" "google.golang.org/protobuf/types/pluginpb" ) @@ -46,25 +51,77 @@ func main() { ParamFunc: flags.Set, } - opts.Run(func(plugin *protogen.Plugin) error { - // Enable "optional" keyword in front of type (e.g. optional string label = 1;) - plugin.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) - if *conf.OutputMode == "source_relative" { - for _, file := range plugin.Files { - if !file.Generate { - continue - } - outfileName := strings.TrimSuffix(file.Desc.Path(), filepath.Ext(file.Desc.Path())) + ".openapi.yaml" - outputFile := plugin.NewGeneratedFile(outfileName, "") - gen := generator.NewOpenAPIv3Generator(plugin, conf, []*protogen.File{file}) - if err := gen.Run(outputFile); err != nil { - return err - } + if err := run(opts, conf); err != nil { + fmt.Fprintf(os.Stderr, "%s: %v\n", filepath.Base(os.Args[0]), err) + os.Exit(1) + } +} + +func run(opts protogen.Options, conf generator.Configuration) error { + if len(os.Args) > 1 { + return fmt.Errorf("unknown argument %q (this program should be run by protoc, not directly)", os.Args[1]) + } + in, err := io.ReadAll(os.Stdin) + if err != nil { + return err + } + req := &pluginpb.CodeGeneratorRequest{} + if err := proto.Unmarshal(in, req); err != nil { + return err + } + // The protogen library requires every .proto file to have a go_package option, + // but this plugin generates OpenAPI specs — not Go code. Inject a synthetic + // go_package for any file that doesn't already have one so users aren't forced + // to add a Go-specific option to their proto files. + for _, fdesc := range req.ProtoFile { + if fdesc.GetOptions().GetGoPackage() == "" { + if fdesc.Options == nil { + fdesc.Options = &descriptorpb.FileOptions{} + } + pkg := fdesc.GetPackage() + if pkg == "" { + pkg = strings.TrimSuffix(filepath.Base(fdesc.GetName()), ".proto") + } + goPackage := "example.com/fake/" + strings.ReplaceAll(pkg, ".", "/") + fdesc.Options.GoPackage = &goPackage + } + } + gen, err := opts.New(req) + if err != nil { + return err + } + if err := generate(gen, conf); err != nil { + gen.Error(err) + } + resp := gen.Response() + out, err := proto.Marshal(resp) + if err != nil { + return err + } + if _, err := os.Stdout.Write(out); err != nil { + return err + } + return nil +} + +func generate(plugin *protogen.Plugin, conf generator.Configuration) error { + // Enable "optional" keyword in front of type (e.g. optional string label = 1;) + plugin.SupportedFeatures = uint64(pluginpb.CodeGeneratorResponse_FEATURE_PROTO3_OPTIONAL) + if *conf.OutputMode == "source_relative" { + for _, file := range plugin.Files { + if !file.Generate { + continue + } + outfileName := strings.TrimSuffix(file.Desc.Path(), filepath.Ext(file.Desc.Path())) + ".openapi.yaml" + outputFile := plugin.NewGeneratedFile(outfileName, "") + gen := generator.NewOpenAPIv3Generator(plugin, conf, []*protogen.File{file}) + if err := gen.Run(outputFile); err != nil { + return err } - } else { - outputFile := plugin.NewGeneratedFile("openapi.yaml", "") - return generator.NewOpenAPIv3Generator(plugin, conf, plugin.Files).Run(outputFile) } - return nil - }) + } else { + outputFile := plugin.NewGeneratedFile("openapi.yaml", "") + return generator.NewOpenAPIv3Generator(plugin, conf, plugin.Files).Run(outputFile) + } + return nil } diff --git a/cmd/protoc-gen-openapi/plugin_test.go b/cmd/protoc-gen-openapi/plugin_test.go index 19507e8b..0cda30e1 100644 --- a/cmd/protoc-gen-openapi/plugin_test.go +++ b/cmd/protoc-gen-openapi/plugin_test.go @@ -44,6 +44,7 @@ var openapiTests = []struct { {name: "OpenAPIv3 Annotations", path: "examples/tests/openapiv3annotations/", protofile: "message.proto"}, {name: "AllOf Wrap Message", path: "examples/tests/allofwrap/", protofile: "message.proto"}, {name: "Additional Bindings", path: "examples/tests/additional_bindings/", protofile: "message.proto"}, + {name: "No go_package option", path: "examples/tests/nogopackage/", protofile: "message.proto"}, } // Set this to true to generate/overwrite the fixtures. Make sure you set it back diff --git a/cmd/protoc-gen-openapi/protoc-gen-openapi b/cmd/protoc-gen-openapi/protoc-gen-openapi new file mode 100755 index 00000000..d80380c3 Binary files /dev/null and b/cmd/protoc-gen-openapi/protoc-gen-openapi differ