Skip to content

Commit

Permalink
protoc-gen-openapi: add flag to generate source_relative yaml (#359)
Browse files Browse the repository at this point in the history
* protoc-gen-openapi: add out_files flag
* rename flag to output_mode
* rename flag field in generator Configuration
* protoc-gen-openapi: examples/tests/noannotations: fix package name
* protoc-gen-openapi: plugin_test: add source_relative generation to fq naming test
* fix c+p error in testdata
* protoc-gen-openapi: test: print protoc args for easier debugging
  • Loading branch information
gfelbing authored Mar 24, 2023
1 parent 0835de8 commit ade94e0
Show file tree
Hide file tree
Showing 6 changed files with 65 additions and 12 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

syntax = "proto3";

package tests.bodymappying.message.v1;
package tests.additional_bindings.message.v1;

import "google/api/annotations.proto";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@

syntax = "proto3";

package tests.mapfields.message.v1;
package tests.noannotations.message.v1;

import "google/api/annotations.proto";

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,15 @@ paths:
content:
application/json:
schema:
$ref: '#/components/schemas/tests.mapfields.message.v1.Message'
$ref: '#/components/schemas/tests.noannotations.message.v1.Message'
required: true
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/tests.mapfields.message.v1.Message'
$ref: '#/components/schemas/tests.noannotations.message.v1.Message'
default:
description: Default error response
content:
Expand Down Expand Up @@ -62,7 +62,7 @@ components:
$ref: '#/components/schemas/google.protobuf.Any'
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).'
tests.mapfields.message.v1.Message:
tests.noannotations.message.v1.Message:
type: object
properties:
id:
Expand Down
14 changes: 9 additions & 5 deletions cmd/protoc-gen-openapi/generator/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type Configuration struct {
EnumType *string
CircularDepth *int
DefaultResponse *bool
OutputMode *string
}

const (
Expand All @@ -60,6 +61,7 @@ type OpenAPIv3Generator struct {
conf Configuration
plugin *protogen.Plugin

inputFiles []*protogen.File
reflect *OpenAPIv3Reflector
generatedSchemas []string // Names of schemas that have already been generated.
linterRulePattern *regexp.Regexp
Expand All @@ -68,11 +70,12 @@ type OpenAPIv3Generator struct {
}

// NewOpenAPIv3Generator creates a new generator for a protoc plugin invocation.
func NewOpenAPIv3Generator(plugin *protogen.Plugin, conf Configuration) *OpenAPIv3Generator {
func NewOpenAPIv3Generator(plugin *protogen.Plugin, conf Configuration, inputFiles []*protogen.File) *OpenAPIv3Generator {
return &OpenAPIv3Generator{
conf: conf,
plugin: plugin,

inputFiles: inputFiles,
reflect: NewOpenAPIv3Reflector(conf),
generatedSchemas: make([]string, 0),
linterRulePattern: regexp.MustCompile(`\(-- .* --\)`),
Expand All @@ -82,14 +85,15 @@ func NewOpenAPIv3Generator(plugin *protogen.Plugin, conf Configuration) *OpenAPI
}

// Run runs the generator.
func (g *OpenAPIv3Generator) Run() error {
func (g *OpenAPIv3Generator) Run(outputFile *protogen.GeneratedFile) error {
d := g.buildDocumentV3()
bytes, err := d.YAMLValue("Generated with protoc-gen-openapi\n" + infoURL)
if err != nil {
return fmt.Errorf("failed to marshal yaml: %s", err.Error())
}
outputFile := g.plugin.NewGeneratedFile("openapi.yaml", "")
outputFile.Write(bytes)
if _, err = outputFile.Write(bytes); err != nil {
return fmt.Errorf("failed to write yaml: %s", err.Error())
}
return nil
}

Expand All @@ -114,7 +118,7 @@ func (g *OpenAPIv3Generator) buildDocumentV3() *v3.Document {
// Go through the files and add the services to the documents, keeping
// track of which schemas are referenced in the response so we can
// add them later.
for _, file := range g.plugin.Files {
for _, file := range g.inputFiles {
if file.Generate {
// Merge any `Document` annotations with the current
extDocument := proto.GetExtension(file.Desc.Options(), v3.E_Document)
Expand Down
22 changes: 20 additions & 2 deletions cmd/protoc-gen-openapi/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ package main

import (
"flag"
"path/filepath"
"strings"

"github.com/google/gnostic/cmd/protoc-gen-openapi/generator"
"google.golang.org/protobuf/compiler/protogen"
Expand All @@ -35,6 +37,7 @@ func main() {
EnumType: flags.String("enum_type", "integer", `type for enum serialization. Use "string" for string-based serialization`),
CircularDepth: flags.Int("depth", 2, "depth of recursion for circular messages"),
DefaultResponse: flags.Bool("default_response", true, `add default response. If "true", automatically adds a default response to operations which use the google.rpc.Status message. Useful if you use envoy or grpc-gateway to transcode as they use this type for their default error responses.`),
OutputMode: flags.String("output_mode", "merged", `output generation mode. By default, a single openapi.yaml is generated at the out folder. Use "source_relative' to generate a separate '[inputfile].openapi.yaml' next to each '[inputfile].proto'.`),
}

opts := protogen.Options{
Expand All @@ -44,7 +47,22 @@ func main() {
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)

return generator.NewOpenAPIv3Generator(plugin, conf).Run()
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
})
}
31 changes: 31 additions & 0 deletions cmd/protoc-gen-openapi/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,13 @@ package main

import (
"errors"
"fmt"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"strings"
"testing"
)

Expand Down Expand Up @@ -114,6 +117,27 @@ func TestOpenAPIProtobufNaming(t *testing.T) {
}

func TestOpenAPIFQSchemaNaming(t *testing.T) {
// create temp directory for source_relative outputs
tempDir := "tmp"
if err := os.MkdirAll(path.Join(tempDir, "examples"), os.ModePerm); err != nil {
t.Fatalf("create tmp directory %+v", err)
}
defer os.RemoveAll(tempDir)
// run protoc with source_relative options on all examples
args := []string{
"-I", "../../",
"-I", "../../third_party",
"-I", "examples",
fmt.Sprintf("--openapi_out=fq_schema_naming=1:%s/examples", tempDir),
"--openapi_opt=output_mode=source_relative",
}
for _, tt := range openapiTests {
args = append(args, path.Join(tt.path, tt.protofile))
}
if err := exec.Command("protoc", args...).Run(); err != nil {
t.Fatalf("protoc %v failed: %+v", strings.Join(args, " "), err)
}

for _, tt := range openapiTests {
fixture := path.Join(tt.path, "openapi_fq_schema_naming.yaml")
if _, err := os.Stat(fixture); errors.Is(err, os.ErrNotExist) {
Expand Down Expand Up @@ -143,6 +167,13 @@ func TestOpenAPIFQSchemaNaming(t *testing.T) {
if err != nil {
t.Fatalf("Diff failed: %+v", err)
}
// Verify that the generated spec matches the source_relative version
sourceRelativeFile := strings.TrimSuffix(tt.protofile, filepath.Ext(tt.protofile)) + ".openapi.yaml"
sourceRelativeOut := path.Join(tempDir, tt.path, sourceRelativeFile)
err = exec.Command("diff", sourceRelativeOut, fixture).Run()
if err != nil {
t.Fatalf("Diff %v %v: %+v", sourceRelativeOut, fixture, err)
}
}
// if the test succeeded, clean up
os.Remove(TEMP_FILE)
Expand Down

0 comments on commit ade94e0

Please sign in to comment.