Skip to content

Commit

Permalink
protoc-gen-swagger: support JSON Schema Validation properties and add…
Browse files Browse the repository at this point in the history
… openapiv2_field option

This change supports [JSON Schema Validation](https://tools.ietf.org/html/draft-fge-json-schema-validation-00) properties.
It allows you to specify some validation rules in `openapiv2_schema` option and `openapiv2_field` option of your .proto files.
  • Loading branch information
co3k committed Aug 6, 2018
1 parent 7951e5b commit 757aaa0
Show file tree
Hide file tree
Showing 8 changed files with 300 additions and 192 deletions.
2 changes: 1 addition & 1 deletion examples/clients/abe/examplepb_a_bit_of_everything.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ type ExamplepbABitOfEverything struct {

SingleNested ABitOfEverythingNested `json:"single_nested,omitempty"`

Uuid string `json:"uuid,omitempty"`
Uuid string `json:"uuid"`

Nested []ABitOfEverythingNested `json:"nested,omitempty"`

Expand Down
303 changes: 153 additions & 150 deletions examples/proto/examplepb/a_bit_of_everything.pb.go

Large diffs are not rendered by default.

3 changes: 2 additions & 1 deletion examples/proto/examplepb/a_bit_of_everything.proto
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ message ABitOfEverything {
json_schema: {
title: "A bit of everything"
description: "Intentionaly complicated message type to cover many features of Protobuf."
required: ["uuid"]
}
external_docs: {
url: "https://github.com/grpc-ecosystem/grpc-gateway";
Expand All @@ -155,7 +156,7 @@ message ABitOfEverything {
}
Nested single_nested = 25;

string uuid = 1;
string uuid = 1 [(grpc.gateway.protoc_gen_swagger.options.openapiv2_field) = {pattern: "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"}];
repeated Nested nested = 2;
float float_value = 3;
double double_value = 4;
Expand Down
8 changes: 6 additions & 2 deletions examples/proto/examplepb/a_bit_of_everything.swagger.json
Original file line number Diff line number Diff line change
Expand Up @@ -1001,7 +1001,8 @@
"$ref": "#/definitions/ABitOfEverythingNested"
},
"uuid": {
"type": "string"
"type": "string",
"pattern": "[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}"
},
"nested": {
"type": "array",
Expand Down Expand Up @@ -1121,7 +1122,10 @@
"externalDocs": {
"description": "Find out more about ABitOfEverything",
"url": "https://github.com/grpc-ecosystem/grpc-gateway"
}
},
"required": [
"uuid"
]
},
"examplepbBody": {
"type": "object",
Expand Down
96 changes: 81 additions & 15 deletions protoc-gen-swagger/genswagger/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,20 @@ func renderMessagesAsDefinition(messages messageMap, d swaggerDefinitionsObject,

// Warning: Make sure not to overwrite any fields already set on the schema type.
schema.ExternalDocs = protoSchema.ExternalDocs
schema.MultipleOf = protoSchema.MultipleOf
schema.Maximum = protoSchema.Maximum
schema.ExclusiveMaximum = protoSchema.ExclusiveMaximum
schema.Minimum = protoSchema.Minimum
schema.ExclusiveMinimum = protoSchema.ExclusiveMinimum
schema.MaxLength = protoSchema.MaxLength
schema.MinLength = protoSchema.MinLength
schema.Pattern = protoSchema.Pattern
schema.MaxItems = protoSchema.MaxItems
schema.MinItems = protoSchema.MinItems
schema.UniqueItems = protoSchema.UniqueItems
schema.MaxProperties = protoSchema.MaxProperties
schema.MinProperties = protoSchema.MinProperties
schema.Required = protoSchema.Required
if protoSchema.schemaCore.Type != "" || protoSchema.schemaCore.Ref != "" {
schema.schemaCore = protoSchema.schemaCore
}
Expand Down Expand Up @@ -318,6 +332,7 @@ func schemaOfField(f *descriptor.Field, reg *descriptor.Registry, refs refMap) s
core = schemaCore{Type: ft.String(), Format: "UNKNOWN"}
}
}

switch aggregate {
case array:
return swaggerSchemaObject{
Expand All @@ -334,7 +349,13 @@ func schemaOfField(f *descriptor.Field, reg *descriptor.Registry, refs refMap) s
AdditionalProperties: &swaggerSchemaObject{schemaCore: core},
}
default:
return swaggerSchemaObject{schemaCore: core}
ret := swaggerSchemaObject{
schemaCore: core,
}
if j, err := extractJSONSchemaFromFieldDescriptor(fd); err == nil {
updateSwaggerObjectFromJSONSchema(&ret, j)
}
return ret
}
}

Expand Down Expand Up @@ -1263,28 +1284,73 @@ func extractSwaggerOptionFromFileDescriptor(file *pbdescriptor.FileDescriptorPro
return opts, nil
}

func swaggerSchemaFromProtoSchema(s *swagger_options.Schema, reg *descriptor.Registry, refs refMap) swaggerSchemaObject {
ret := swaggerSchemaObject{
ExternalDocs: protoExternalDocumentationToSwaggerExternalDocumentation(s.GetExternalDocs()),
Title: s.GetJsonSchema().GetTitle(),
Description: s.GetJsonSchema().GetDescription(),
// TODO(johanbrandhorst): Add more fields?
func extractJSONSchemaFromFieldDescriptor(fd *pbdescriptor.FieldDescriptorProto) (*swagger_options.JSONSchema, error) {
if fd.Options == nil {
return nil, nil
}
if s.GetJsonSchema().GetRef() != "" {
swaggerName := fullyQualifiedNameToSwaggerName(s.GetJsonSchema().GetRef(), reg)
if !proto.HasExtension(fd.Options, swagger_options.E_Openapiv2Field) {
return nil, nil
}
ext, err := proto.GetExtension(fd.Options, swagger_options.E_Openapiv2Field)
if err != nil {
return nil, err
}
opts, ok := ext.(*swagger_options.JSONSchema)
if !ok {
return nil, fmt.Errorf("extension is %T; want a JSONSchema object", ext)
}
return opts, nil
}

func protoJSONSchemaToSwaggerSchemaCore(j *swagger_options.JSONSchema, reg *descriptor.Registry, refs refMap) schemaCore {
ret := schemaCore{}

if j.GetRef() != "" {
swaggerName := fullyQualifiedNameToSwaggerName(j.GetRef(), reg)
if swaggerName != "" {
ret.schemaCore.Ref = "#/definitions/" + swaggerName
ret.Ref = "#/definitions/" + swaggerName
if refs != nil {
refs[s.GetJsonSchema().GetRef()] = struct{}{}
refs[j.GetRef()] = struct{}{}
}
} else {
ret.schemaCore.Ref += s.GetJsonSchema().GetRef()
ret.Ref += j.GetRef()
}
} else {
f, t := protoJSONSchemaTypeToFormat(s.GetJsonSchema().GetType())
ret.schemaCore.Format = f
ret.schemaCore.Type = t
f, t := protoJSONSchemaTypeToFormat(j.GetType())
ret.Format = f
ret.Type = t
}

return ret
}

func updateSwaggerObjectFromJSONSchema(s *swaggerSchemaObject, j *swagger_options.JSONSchema) {
s.Title = j.GetTitle()
s.Description = j.GetDescription()
s.MultipleOf = j.GetMultipleOf()
s.Maximum = j.GetMaximum()
s.ExclusiveMaximum = j.GetExclusiveMaximum()
s.Minimum = j.GetMinimum()
s.ExclusiveMinimum = j.GetExclusiveMinimum()
s.MaxLength = j.GetMaxLength()
s.MinLength = j.GetMinLength()
s.Pattern = j.GetPattern()
s.MaxItems = j.GetMaxItems()
s.MinItems = j.GetMinItems()
s.UniqueItems = j.GetUniqueItems()
s.MaxProperties = j.GetMaxProperties()
s.MinProperties = j.GetMinProperties()
s.Required = j.GetRequired()
}

func swaggerSchemaFromProtoSchema(s *swagger_options.Schema, reg *descriptor.Registry, refs refMap) swaggerSchemaObject {
ret := swaggerSchemaObject{
ExternalDocs: protoExternalDocumentationToSwaggerExternalDocumentation(s.GetExternalDocs()),
}

ret.schemaCore = protoJSONSchemaToSwaggerSchemaCore(s.GetJsonSchema(), reg, refs)
updateSwaggerObjectFromJSONSchema(&ret, s.GetJsonSchema())

return ret
}

Expand Down
15 changes: 15 additions & 0 deletions protoc-gen-swagger/genswagger/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,21 @@ type swaggerSchemaObject struct {
Title string `json:"title,omitempty"`

ExternalDocs *swaggerExternalDocumentationObject `json:"externalDocs,omitempty"`

MultipleOf float64 `json:"multiple_of,omitempty"`
Maximum float64 `json:"maximum,omitempty"`
ExclusiveMaximum bool `json:"exclusive_maximum,omitempty"`
Minimum float64 `json:"minimum,omitempty"`
ExclusiveMinimum bool `json:"exclusive_minimum,omitempty"`
MaxLength uint64 `json:"max_length,omitempty"`
MinLength uint64 `json:"min_length,omitempty"`
Pattern string `json:"pattern,omitempty"`
MaxItems uint64 `json:"max_items,omitempty"`
MinItems uint64 `json:"min_items,omitempty"`
UniqueItems bool `json:"unique_items,omitempty"`
MaxProperties uint64 `json:"max_properties,omitempty"`
MinProperties uint64 `json:"min_properties,omitempty"`
Required []string `json:"required,omitempty"`
}

// http://swagger.io/specification/#referenceObject
Expand Down
58 changes: 35 additions & 23 deletions protoc-gen-swagger/options/annotations.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions protoc-gen-swagger/options/annotations.proto
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,10 @@ extend google.protobuf.ServiceOptions {
// different descriptor messages.
Tag openapiv2_tag = 1042;
}
extend google.protobuf.FieldOptions {
// ID assigned by [email protected] for grpc-gateway project.
//
// All IDs are the same, as assigned. It is okay that they are the same, as they extend
// different descriptor messages.
JSONSchema openapiv2_field = 1042;
}

0 comments on commit 757aaa0

Please sign in to comment.