Skip to content

Commit

Permalink
Merge pull request #4 from solo-io/configure-types-to-skip-schemas
Browse files Browse the repository at this point in the history
Configure types to skip schemas
  • Loading branch information
sam-heilbron authored Jun 17, 2022
2 parents 41ace51 + bcd398a commit 34a2606
Show file tree
Hide file tree
Showing 5 changed files with 86 additions and 53 deletions.
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,4 +66,10 @@ Other supported options are:
* `use_ref`
* when set to `true`, the output uses the `$ref` field in OpenAPI spec to reference other schemas.
* `yaml`
* when set to `true`, the output is in yaml file.
* when set to `true`, the output is in yaml file.
* `include_description`
* when set to `true`, the openapi schema will include descriptions, generated from the proto message comment.
* `enum_as_int_or_string`
* when set to `true`, the openapi schema will include `x-kubernetes-int-or-string` on enums.
* `additional_empty_schemas`
* a `+` separated list of message names (`core.solo.io.Status`), whose generated schema should be an empty object that accepts all values.
5 changes: 5 additions & 0 deletions changelog/v0.1.0/support-empty-schema.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
changelog:
- type: BREAKING_CHANGE
description: Support injecting a set of message names to generate empty validation schema
issueLink: https://github.com/solo-io/gloo/issues/4789
resolvesIssue: false
9 changes: 8 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
module github.com/solo-io/protoc-gen-openapi

go 1.16
go 1.18

require (
github.com/getkin/kin-openapi v0.80.0
github.com/ghodss/yaml v1.0.0
github.com/golang/protobuf v1.3.2
)

require (
github.com/go-openapi/jsonpointer v0.19.5 // indirect
github.com/go-openapi/swag v0.19.5 // indirect
github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e // indirect
gopkg.in/yaml.v2 v2.3.0 // indirect
)
22 changes: 12 additions & 10 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ package main

import (
"fmt"
"strconv"
"strings"

"github.com/solo-io/protoc-gen-openapi/pkg/protocgen"
Expand Down Expand Up @@ -49,9 +48,9 @@ func generate(request plugin.CodeGeneratorRequest) (*plugin.CodeGeneratorRespons
singleFile := false
yaml := false
useRef := false
maxCharactersInDescription := 0
includeDescription := true
enumAsIntOrString := false
var messagesWithEmptySchema []string

p := extractParams(request.GetParameter())
for k, v := range p {
Expand Down Expand Up @@ -87,12 +86,6 @@ func generate(request plugin.CodeGeneratorRequest) (*plugin.CodeGeneratorRespons
default:
return nil, fmt.Errorf("unknown value '%s' for use_ref", v)
}
} else if k == "max_description_characters" {
var err error
maxCharactersInDescription, err = strconv.Atoi(v)
if err != nil {
return nil, fmt.Errorf("unknown value '%s' for max_description_characters", v)
}
} else if k == "include_description" {
switch strings.ToLower(v) {
case "true":
Expand All @@ -111,6 +104,8 @@ func generate(request plugin.CodeGeneratorRequest) (*plugin.CodeGeneratorRespons
default:
return nil, fmt.Errorf("unknown value '%s' for enum_as_int_or_string", v)
}
} else if k == "additional_empty_schema" {
messagesWithEmptySchema = strings.Split(v, "+")
} else {
return nil, fmt.Errorf("unknown argument '%s' specified", k)
}
Expand All @@ -128,11 +123,18 @@ func generate(request plugin.CodeGeneratorRequest) (*plugin.CodeGeneratorRespons
}

descriptionConfiguration := &DescriptionConfiguration{
MaxDescriptionCharacters: maxCharactersInDescription,
IncludeDescriptionInSchema: includeDescription,
}

g := newOpenAPIGenerator(m, perFile, singleFile, yaml, useRef, descriptionConfiguration, enumAsIntOrString)
g := newOpenAPIGenerator(
m,
perFile,
singleFile,
yaml,
useRef,
descriptionConfiguration,
enumAsIntOrString,
messagesWithEmptySchema)
return g.generateOutput(filesToGen)
}

Expand Down
95 changes: 54 additions & 41 deletions openapiGenerator.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,29 +35,11 @@ import (
// Some special types with predefined schemas.
// This is to catch cases where solo apis contain recursive definitions
// Normally these would result in stack-overflow errors when generating the open api schema
// The imperfect solution, is to just genrate an empty object for these types
// The imperfect solution, is to just generate an empty object for these types
var specialSoloTypes = map[string]openapi3.Schema{
"core.solo.io.Status": {
Type: openapi3.TypeObject,
Properties: make(map[string]*openapi3.SchemaRef),
ExtensionProps: openapi3.ExtensionProps{
Extensions: map[string]interface{}{
"x-kubernetes-preserve-unknown-fields": true,
},
},
},
"core.solo.io.Metadata": {
Type: openapi3.TypeObject,
},
"ratelimit.api.solo.io.Descriptor": {
Type: openapi3.TypeObject,
Properties: make(map[string]*openapi3.SchemaRef),
ExtensionProps: openapi3.ExtensionProps{
Extensions: map[string]interface{}{
"x-kubernetes-preserve-unknown-fields": true,
},
},
},
"google.protobuf.ListValue": *openapi3.NewArraySchema().WithItems(openapi3.NewObjectSchema()),
"google.protobuf.Struct": {
Type: openapi3.TypeObject,
Expand Down Expand Up @@ -116,31 +98,68 @@ type openapiGenerator struct {
// @solo.io customization to support enum validation schemas with int or string values
// we need to support this since some controllers marshal enums as integers and others as strings
enumAsIntOrString bool

// @solo.io customizations to define schemas for certain messages
customSchemasByMessageName map[string]openapi3.Schema
}

type DescriptionConfiguration struct {
// Whether or not to include a description in the generated open api schema
IncludeDescriptionInSchema bool

// The maximum number of characters to include in a description
// If IncludeDescriptionsInSchema is set to false, this will be ignored
// A 0 value will be interpreted as "include all characters"
// Default: 0
MaxDescriptionCharacters int
}

func newOpenAPIGenerator(model *protomodel.Model, perFile bool, singleFile bool, yaml bool, useRef bool, descriptionConfiguration *DescriptionConfiguration, enumAsIntOrString bool) *openapiGenerator {
func newOpenAPIGenerator(
model *protomodel.Model,
perFile bool,
singleFile bool,
yaml bool,
useRef bool,
descriptionConfiguration *DescriptionConfiguration,
enumAsIntOrString bool,
messagesWithEmptySchema []string,
) *openapiGenerator {
return &openapiGenerator{
model: model,
perFile: perFile,
singleFile: singleFile,
yaml: yaml,
useRef: useRef,
descriptionConfiguration: descriptionConfiguration,
enumAsIntOrString: enumAsIntOrString,
model: model,
perFile: perFile,
singleFile: singleFile,
yaml: yaml,
useRef: useRef,
descriptionConfiguration: descriptionConfiguration,
enumAsIntOrString: enumAsIntOrString,
customSchemasByMessageName: buildCustomSchemasByMessageName(messagesWithEmptySchema),
}
}

// buildCustomSchemasByMessageName name returns a mapping of message name to a pre-defined openapi schema
// It includes:
// 1. `specialSoloTypes`, a set of pre-defined schemas
// 2. `messagesWithEmptySchema`, a list of messages that are injected at runtime that should contain
// and empty schema which accepts and preserves all fields
func buildCustomSchemasByMessageName(messagesWithEmptySchema []string) map[string]openapi3.Schema {
schemasByMessageName := make(map[string]openapi3.Schema)

// Initialize the hard-coded values
for name, schema := range specialSoloTypes {
schemasByMessageName[name] = schema
}

// Add the messages that were injected at runtime
for _, messageName := range messagesWithEmptySchema {
emptyMessage := openapi3.Schema{
Type: openapi3.TypeObject,
Properties: make(map[string]*openapi3.SchemaRef),
ExtensionProps: openapi3.ExtensionProps{
Extensions: map[string]interface{}{
"x-kubernetes-preserve-unknown-fields": true,
},
},
}
schemasByMessageName[messageName] = emptyMessage
}

return schemasByMessageName
}

func (g *openapiGenerator) generateOutput(filesToGen map[*protomodel.FileDescriptor]bool) (*plugin.CodeGeneratorResponse, error) {
response := plugin.CodeGeneratorResponse{}

Expand Down Expand Up @@ -438,13 +457,7 @@ func (g *openapiGenerator) generateDescription(desc protomodel.CoreDesc) string
return ""
}

fullDescription := strings.Join(strings.Fields(t), " ")
maxCharacters := g.descriptionConfiguration.MaxDescriptionCharacters
if maxCharacters > 0 && len(fullDescription) > maxCharacters {
// return the first [maxCharacters] characters, including an ellipsis to mark that it has been truncated
return fmt.Sprintf("%s...", fullDescription[0:maxCharacters])
}
return fullDescription
return strings.Join(strings.Fields(t), " ")
}

func (g *openapiGenerator) fieldType(field *protomodel.FieldDescriptor) *openapi3.Schema {
Expand Down Expand Up @@ -474,7 +487,7 @@ func (g *openapiGenerator) fieldType(field *protomodel.FieldDescriptor) *openapi

case descriptor.FieldDescriptorProto_TYPE_MESSAGE:
msg := field.FieldType.(*protomodel.MessageDescriptor)
if soloSchema, ok := specialSoloTypes[g.absoluteName(msg)]; ok {
if soloSchema, ok := g.customSchemasByMessageName[g.absoluteName(msg)]; ok {
// Allow for defining special Solo types
schema = g.generateSoloMessageSchema(msg, &soloSchema)
} else if msg.GetOptions().GetMapEntry() {
Expand Down

0 comments on commit 34a2606

Please sign in to comment.