Skip to content

Commit

Permalink
protoc-gen-openapi/jsonschema: Add support for string-based enums (#312)
Browse files Browse the repository at this point in the history
* protoc-gen-openapi: Expose configuration for enum serialization type
* protoc-gen-jsonschema: Expose configuration for enum serialization type
  • Loading branch information
adolfo authored Mar 14, 2022
1 parent 3334dd9 commit a03001a
Show file tree
Hide file tree
Showing 9 changed files with 268 additions and 17 deletions.
19 changes: 15 additions & 4 deletions cmd/protoc-gen-jsonschema/generator/json-schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,10 @@ func init() {
}

type Configuration struct {
BaseURL *string
Version *string
Naming *string
BaseURL *string
Version *string
Naming *string
EnumType *string
}

// JSONSchemaGenerator holds internal state needed to generate the JSON Schema documents for a transcoded Protocol Buffer service.
Expand Down Expand Up @@ -222,7 +223,17 @@ func (g *JSONSchemaGenerator) schemaOrReferenceForField(field protoreflect.Field
kindSchema = &jsonschema.Schema{Type: &jsonschema.StringOrStringArray{String: &typeInteger}, Format: &format}

case protoreflect.EnumKind:
kindSchema = &jsonschema.Schema{Type: &jsonschema.StringOrStringArray{String: &typeInteger}, Format: &formatEnum}
kindSchema = &jsonschema.Schema{Format: &formatEnum}
if g.conf.EnumType != nil && *g.conf.EnumType == typeString {
kindSchema.Type = &jsonschema.StringOrStringArray{String: &typeString}
kindSchema.Enumeration = &[]jsonschema.SchemaEnumValue{}
for i := 0; i < field.Enum().Values().Len(); i++ {
name := string(field.Enum().Values().Get(i).Name())
*kindSchema.Enumeration = append(*kindSchema.Enumeration, jsonschema.SchemaEnumValue{String: &name})
}
} else {
kindSchema.Type = &jsonschema.StringOrStringArray{String: &typeInteger}
}

case protoreflect.BoolKind:
kindSchema = &jsonschema.Schema{Type: &jsonschema.StringOrStringArray{String: &typeBoolean}}
Expand Down
9 changes: 5 additions & 4 deletions cmd/protoc-gen-jsonschema/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package main
import (
"flag"

"github.com/google/gnostic/cmd/protoc-gen-jsonschema/generator"
"github.com/google/gnostic/cmd/protoc-gen-jsonschema/generator"
"google.golang.org/protobuf/compiler/protogen"
"google.golang.org/protobuf/types/pluginpb"
)
Expand All @@ -27,9 +27,10 @@ var flags flag.FlagSet

func main() {
conf := generator.Configuration{
BaseURL: flags.String("baseurl", "", "the base url to use in schema ids"),
Version: flags.String("version", "http://json-schema.org/draft-07/schema#", "schema version URL used in $schema. Currently supported: draft-06, draft-07"),
Naming: flags.String("naming", "json", `naming convention. Use "proto" for passing names directly from the proto files`),
BaseURL: flags.String("baseurl", "", "the base url to use in schema ids"),
Version: flags.String("version", "http://json-schema.org/draft-07/schema#", "schema version URL used in $schema. Currently supported: draft-06, draft-07"),
Naming: flags.String("naming", "json", `naming convention. Use "proto" for passing names directly from the proto files`),
EnumType: flags.String("enum_type", "integer", `type for enum serialization. Use "string" for string-based serialization`),
}

opts := protogen.Options{
Expand Down
53 changes: 50 additions & 3 deletions cmd/protoc-gen-jsonschema/plugin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package main

import (
"errors"
"os"
"os/exec"
"path"
Expand All @@ -38,10 +39,15 @@ var jsonschemaTests = []struct {
{name: "JSON options", path: "examples/tests/jsonoptions/", pkg: "", protofile: "message.proto"},
{name: "Embedded messages", path: "examples/tests/embedded/", pkg: "", protofile: "message.proto"},
{name: "Protobuf types", path: "examples/tests/protobuftypes/", pkg: "", protofile: "message.proto"},
{name: "Enum Options", path: "examples/tests/enumoptions/", pkg: "", protofile: "message.proto"},
}

func TestJSONSchemaProtobufNaming(t *testing.T) {
for _, tt := range jsonschemaTests {
schemasPath := path.Join(tt.path, "schemas_proto")
if _, err := os.Stat(schemasPath); errors.Is(err, os.ErrNotExist) {
continue
}
t.Run(tt.name, func(t *testing.T) {
os.RemoveAll(testSchemasPath)
os.MkdirAll(testSchemasPath, 0777)
Expand All @@ -59,7 +65,7 @@ func TestJSONSchemaProtobufNaming(t *testing.T) {
}

// Verify that the generated spec matches our expected version.
err = exec.Command("diff", testSchemasPath, path.Join(tt.path, "schemas_proto")).Run()
err = exec.Command("diff", testSchemasPath, schemasPath).Run()
if err != nil {
t.Fatalf("Diff failed: %+v", err)
}
Expand All @@ -72,6 +78,10 @@ func TestJSONSchemaProtobufNaming(t *testing.T) {

func TestJSONSchemaJSONNaming(t *testing.T) {
for _, tt := range jsonschemaTests {
schemasPath := path.Join(tt.path, "schemas_json")
if _, err := os.Stat(schemasPath); errors.Is(err, os.ErrNotExist) {
continue
}
t.Run(tt.name, func(t *testing.T) {
os.RemoveAll(testSchemasPath)
os.MkdirAll(testSchemasPath, 0777)
Expand All @@ -88,7 +98,7 @@ func TestJSONSchemaJSONNaming(t *testing.T) {
}

// Verify that the generated spec matches our expected version.
err = exec.Command("diff", testSchemasPath, path.Join(tt.path, "schemas_json")).Run()
err = exec.Command("diff", testSchemasPath, schemasPath).Run()
if err != nil {
t.Fatalf("Diff failed: %+v", err)
}
Expand All @@ -102,8 +112,11 @@ func TestJSONSchemaJSONNaming(t *testing.T) {
// Meta... Test the tests
func TestJSONSchemaJSONNamingSchemas(t *testing.T) {
for _, tt := range jsonschemaTests {
schemasPath := path.Join(tt.path, "schemas_json")
if _, err := os.Stat(schemasPath); errors.Is(err, os.ErrNotExist) {
continue
}
t.Run(tt.name, func(t *testing.T) {
schemasPath := path.Join(tt.path, "schemas_json")
schemaFiles, err := os.ReadDir(schemasPath)
if err != nil {
t.Fatal(err)
Expand Down Expand Up @@ -155,3 +168,37 @@ func TestJSONSchemaJSONNamingSchemas(t *testing.T) {
})
}
}

func TestJSONSchemaStringEnums(t *testing.T) {
for _, tt := range jsonschemaTests {
schemasPath := path.Join(tt.path, "schemas_string_enum")
if _, err := os.Stat(schemasPath); errors.Is(err, os.ErrNotExist) {
continue
}
t.Run(tt.name, func(t *testing.T) {
os.RemoveAll(testSchemasPath)
os.MkdirAll(testSchemasPath, 0777)
// Run protoc and the protoc-gen-jsonschema plugin to generate JSON Schema(s) with JSON naming.
err := exec.Command("protoc",
"-I", "../../",
"-I", "../../third_party",
"-I", "examples",
path.Join(tt.path, tt.protofile),
"--jsonschema_opt=baseurl=http://example.com/schemas",
"--jsonschema_opt=enum_type=string",
"--jsonschema_out="+testSchemasPath).Run()
if err != nil {
t.Fatalf("protoc failed: %+v", err)
}

// Verify that the generated spec matches our expected version.
err = exec.Command("diff", testSchemasPath, schemasPath).Run()
if err != nil {
t.Fatalf("Diff failed: %+v", err)
}

// if the test succeeded, clean up
os.RemoveAll(testSchemasPath)
})
}
}
40 changes: 40 additions & 0 deletions cmd/protoc-gen-openapi/examples/tests/enumoptions/message.proto
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2020 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.enumoptions.message.v1;

import "google/api/annotations.proto";

option go_package = "github.com/google/gnostic/apps/protoc-gen-openapi/examples/tests/enumoptions/message/v1;message";

// Messaging service
service Messaging {
rpc CreateMessage(Message) returns (Message) {
option (google.api.http) = {
post : "/v1/messages/{message_id}"
body : "body_text"
};
}
}
message Message {
Kind kind = 1;
}
enum Kind {
UNKNOWN_KIND = 0;
KIND_1 = 1;
KIND_2 = 2;
}
46 changes: 46 additions & 0 deletions cmd/protoc-gen-openapi/examples/tests/enumoptions/openapi.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Generated with protoc-gen-openapi
# https://github.com/google/gnostic/tree/master/cmd/protoc-gen-openapi

openapi: 3.0.3
info:
title: Messaging API
description: Messaging service
version: 0.0.1
paths:
/v1/messages/{message_id}:
post:
tags:
- Messaging
operationId: Messaging_CreateMessage
parameters:
- name: message_id
in: path
required: true
schema:
type: string
- name: kind
in: query
schema:
type: integer
format: enum
requestBody:
content:
application/json: {}
required: true
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Message'
components:
schemas:
Message:
type: object
properties:
kind:
type: integer
format: enum
tags:
- name: Messaging
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Generated with protoc-gen-openapi
# https://github.com/google/gnostic/tree/master/cmd/protoc-gen-openapi

openapi: 3.0.3
info:
title: Messaging API
description: Messaging service
version: 0.0.1
paths:
/v1/messages/{message_id}:
post:
tags:
- Messaging
operationId: Messaging_CreateMessage
parameters:
- name: message_id
in: path
required: true
schema:
type: string
- name: kind
in: query
schema:
enum:
- UNKNOWN_KIND
- KIND_1
- KIND_2
type: string
format: enum
requestBody:
content:
application/json: {}
required: true
responses:
"200":
description: OK
content:
application/json:
schema:
$ref: '#/components/schemas/Message'
components:
schemas:
Message:
type: object
properties:
kind:
enum:
- UNKNOWN_KIND
- KIND_1
- KIND_2
type: string
format: enum
tags:
- name: Messaging
15 changes: 14 additions & 1 deletion cmd/protoc-gen-openapi/generator/openapi-v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type Configuration struct {
Title *string
Description *string
Naming *string
EnumType *string
CircularDepth *int
}

Expand Down Expand Up @@ -842,9 +843,21 @@ func (g *OpenAPIv3Generator) schemaOrReferenceForField(field protoreflect.FieldD
Schema: &v3.Schema{Type: "integer", Format: kind.String()}}}

case protoreflect.EnumKind:
s := &v3.Schema{Format: "enum"}
if g.conf.EnumType != nil && *g.conf.EnumType == "string" {
s.Type = "string"
s.Enum = make([]*v3.Any, 0, field.Enum().Values().Len())
for i := 0; i < field.Enum().Values().Len(); i++ {
s.Enum = append(s.Enum, &v3.Any{
Yaml: string(field.Enum().Values().Get(i).Name()),
})
}
} else {
s.Type = "integer"
}
kindSchema = &v3.SchemaOrReference{
Oneof: &v3.SchemaOrReference_Schema{
Schema: &v3.Schema{Type: "integer", Format: "enum"}}}
Schema: s}}

case protoreflect.BoolKind:
kindSchema = &v3.SchemaOrReference{
Expand Down
5 changes: 3 additions & 2 deletions cmd/protoc-gen-openapi/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ package main
import (
"flag"

"github.com/google/gnostic/cmd/protoc-gen-openapi/generator"
"github.com/google/gnostic/cmd/protoc-gen-openapi/generator"
"google.golang.org/protobuf/compiler/protogen"
"google.golang.org/protobuf/types/pluginpb"
)
Expand All @@ -31,7 +31,8 @@ func main() {
Title: flags.String("title", "", "name of the API"),
Description: flags.String("description", "", "description of the API"),
Naming: flags.String("naming", "json", `naming convention. Use "proto" for passing names directly from the proto files`),
CircularDepth: flags.Int("depth", 2, `depth of recursion for circular messages`),
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"),
}

opts := protogen.Options{
Expand Down
Loading

0 comments on commit a03001a

Please sign in to comment.