From d2fd8af03e7dedfe193558787acd58a363d27f31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Vu=C4=8Dica?= Date: Sun, 17 Apr 2016 02:14:10 +0100 Subject: [PATCH 01/13] Generate Swagger description for service methods using proto comments. While this is a first step in resolving gengo/grpc-gateway#128, this needs to be cleaned up, and the same approach needs to be used for messages, message fields, et al. echo_service.proto has been annotated with extra comments in order to demo the new descriptions. Only the Swagger example has been regenerated, as my local generator does not output all the expected fields in proto struct tags. --- examples/examplepb/echo_service.proto | 5 +++ examples/examplepb/echo_service.swagger.json | 2 + protoc-gen-swagger/genswagger/template.go | 39 +++++++++++++++++++- protoc-gen-swagger/genswagger/types.go | 1 + 4 files changed, 45 insertions(+), 2 deletions(-) diff --git a/examples/examplepb/echo_service.proto b/examples/examplepb/echo_service.proto index 5e44b2827c6..83ebe704ed3 100644 --- a/examples/examplepb/echo_service.proto +++ b/examples/examplepb/echo_service.proto @@ -4,16 +4,21 @@ package gengo.grpc.gateway.examples.examplepb; import "google/api/annotations.proto"; +// SimpleMessage represents a simple message sent to the Echo service. message SimpleMessage { + // Id represents the message identifier. string id = 1; } +// Echo service responds to incoming echo requests. service EchoService { + // Echo method receives a simple message and returns it. rpc Echo(SimpleMessage) returns (SimpleMessage) { option (google.api.http) = { post: "/v1/example/echo/{id}" }; } + // EchoBody method receives a simple message and returns it. rpc EchoBody(SimpleMessage) returns (SimpleMessage) { option (google.api.http) = { post: "/v1/example/echo_body" diff --git a/examples/examplepb/echo_service.swagger.json b/examples/examplepb/echo_service.swagger.json index 76a905721d6..3db1c964c7c 100644 --- a/examples/examplepb/echo_service.swagger.json +++ b/examples/examplepb/echo_service.swagger.json @@ -18,6 +18,7 @@ "/v1/example/echo/{id}": { "post": { "summary": "EchoService.Echo", + "description": "Echo method receives a simple message and returns it.", "operationId": "Echo", "responses": { "default": { @@ -44,6 +45,7 @@ "/v1/example/echo_body": { "post": { "summary": "EchoService.EchoBody", + "description": "EchoBody method receives a simple message and returns it.", "operationId": "EchoBody", "responses": { "default": { diff --git a/protoc-gen-swagger/genswagger/template.go b/protoc-gen-swagger/genswagger/template.go index 26a272cd34e..ef2075e4730 100644 --- a/protoc-gen-swagger/genswagger/template.go +++ b/protoc-gen-swagger/genswagger/template.go @@ -6,6 +6,7 @@ import ( "fmt" "reflect" "regexp" + "strconv" "strings" "github.com/gengo/grpc-gateway/protoc-gen-grpc-gateway/descriptor" @@ -304,8 +305,9 @@ func templateToSwaggerPath(path string) string { } func renderServices(services []*descriptor.Service, paths swaggerPathsObject, reg *descriptor.Registry) error { - for _, svc := range services { - for _, meth := range svc.Methods { + // Correctness of svcIdx and methIdx depends on 'services' containing the services in the same order as the 'file.Service' array. + for svcIdx, svc := range services { + for methIdx, meth := range svc.Methods { if meth.GetClientStreaming() || meth.GetServerStreaming() { return fmt.Errorf(`service uses streaming, which is not currently supported. Maybe you would like to implement it? It wouldn't be that hard and we don't bite. Why don't you send a pull request to https://github.com/gengo/grpc-gateway?`) } @@ -415,8 +417,41 @@ func renderServices(services []*descriptor.Service, paths swaggerPathsObject, re if !ok { pathItemObject = swaggerPathItemObject{} } + + // TODO(ivucica): make this a module-level function and use elsewhere. + protoPath := func(descriptorType reflect.Type, what string) int32 { + // TODO(ivucica): handle errors obtaining any of the following. + field, ok := descriptorType.Elem().FieldByName(what) + if !ok { + // TODO(ivucica): consider being more graceful. + panic(fmt.Errorf("Could not find type id for %s.", what)) + } + pbtag := field.Tag.Get("protobuf") + if pbtag == "" { + // TODO(ivucica): consider being more graceful. + panic(fmt.Errorf("No protobuf tag on %s.", what)) + } + // TODO(ivucica): handle error + path, _ := strconv.Atoi(strings.Split(pbtag, ",")[1]) + + return int32(path) + } + methDescription := "" + for _, loc := range svc.File.SourceCodeInfo.Location { + if len(loc.Path) < 4 { + continue + } + if loc.Path[0] == protoPath(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "Service") && loc.Path[1] == int32(svcIdx) && loc.Path[2] == protoPath(reflect.TypeOf((*pbdescriptor.ServiceDescriptorProto)(nil)), "Method") && loc.Path[3] == int32(methIdx) { + if loc.LeadingComments != nil { + methDescription = strings.TrimRight(*loc.LeadingComments, "\n") + methDescription = strings.TrimLeft(methDescription, " ") + } + break + } + } operationObject := &swaggerOperationObject{ Summary: fmt.Sprintf("%s.%s", svc.GetName(), meth.GetName()), + Description: methDescription, Tags: []string{svc.GetName()}, OperationId: fmt.Sprintf("%s", meth.GetName()), Parameters: parameters, diff --git a/protoc-gen-swagger/genswagger/types.go b/protoc-gen-swagger/genswagger/types.go index fd441683b1b..a3844420342 100644 --- a/protoc-gen-swagger/genswagger/types.go +++ b/protoc-gen-swagger/genswagger/types.go @@ -46,6 +46,7 @@ type swaggerPathItemObject struct { // http://swagger.io/specification/#operationObject type swaggerOperationObject struct { Summary string `json:"summary"` + Description string `json:"description,omitempty"` OperationId string `json:"operationId"` Responses swaggerResponsesObject `json:"responses"` Parameters swaggerParametersObject `json:"parameters,omitempty"` From 06b3e56476871bd5b380fa75509c204c4e5d982c Mon Sep 17 00:00:00 2001 From: Yukinari Toyota Date: Wed, 27 Apr 2016 20:51:43 +0900 Subject: [PATCH 02/13] Generate Swagger description for message fields using proto comments. --- examples/examplepb/echo_service.swagger.json | 3 +- .../streamless_everything.swagger.json | 3 +- .../descriptor/registry.go | 3 +- protoc-gen-grpc-gateway/descriptor/types.go | 3 + protoc-gen-swagger/genswagger/template.go | 62 ++++++++++++------- protoc-gen-swagger/genswagger/types.go | 2 + 6 files changed, 51 insertions(+), 25 deletions(-) diff --git a/examples/examplepb/echo_service.swagger.json b/examples/examplepb/echo_service.swagger.json index 3db1c964c7c..6fc2437f051 100644 --- a/examples/examplepb/echo_service.swagger.json +++ b/examples/examplepb/echo_service.swagger.json @@ -76,7 +76,8 @@ "properties": { "id": { "type": "string", - "format": "string" + "format": "string", + "description": "Id represents the message identifier." } } } diff --git a/examples/examplepb/streamless_everything.swagger.json b/examples/examplepb/streamless_everything.swagger.json index 50806ce071a..847e9e867df 100644 --- a/examples/examplepb/streamless_everything.swagger.json +++ b/examples/examplepb/streamless_everything.swagger.json @@ -429,7 +429,8 @@ }, "uint32_value": { "type": "integer", - "format": "int64" + "format": "int64", + "description": "TODO(yugui) add bytes_value" }, "uint64_value": { "type": "integer", diff --git a/protoc-gen-grpc-gateway/descriptor/registry.go b/protoc-gen-grpc-gateway/descriptor/registry.go index 5d7091c51e7..ee032f1edbb 100644 --- a/protoc-gen-grpc-gateway/descriptor/registry.go +++ b/protoc-gen-grpc-gateway/descriptor/registry.go @@ -99,11 +99,12 @@ func (r *Registry) loadFile(file *descriptor.FileDescriptorProto) { } func (r *Registry) registerMsg(file *File, outerPath []string, msgs []*descriptor.DescriptorProto) { - for _, md := range msgs { + for i, md := range msgs { m := &Message{ File: file, Outers: outerPath, DescriptorProto: md, + Index: i, } for _, fd := range md.GetField() { m.Fields = append(m.Fields, &Field{ diff --git a/protoc-gen-grpc-gateway/descriptor/types.go b/protoc-gen-grpc-gateway/descriptor/types.go index 1bb232b75e5..cea2db84516 100644 --- a/protoc-gen-grpc-gateway/descriptor/types.go +++ b/protoc-gen-grpc-gateway/descriptor/types.go @@ -58,6 +58,9 @@ type Message struct { Outers []string *descriptor.DescriptorProto Fields []*Field + + // Index is proto path index of this message in File. + Index int } // FQMN returns a fully qualified message name of this message. diff --git a/protoc-gen-swagger/genswagger/template.go b/protoc-gen-swagger/genswagger/template.go index ef2075e4730..8c2dd5fcb2c 100644 --- a/protoc-gen-swagger/genswagger/template.go +++ b/protoc-gen-swagger/genswagger/template.go @@ -54,7 +54,7 @@ func renderMessagesAsDefinition(messages messageMap, d swaggerDefinitionsObject, object := swaggerSchemaObject{ Properties: map[string]swaggerSchemaObject{}, } - for _, field := range msg.Fields { + for fieldIdx, field := range msg.Fields { var fieldType, fieldFormat string primitive := true // Field type and format from http://swagger.io/specification/ in the @@ -141,6 +141,20 @@ func renderMessagesAsDefinition(messages messageMap, d swaggerDefinitionsObject, fieldFormat = "UNKNOWN" } + fieldDescription := "" + for _, loc := range msg.File.SourceCodeInfo.Location { + if len(loc.Path) < 4 { + continue + } + if loc.Path[0] == protoPath(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "MessageType") && loc.Path[1] == int32(msg.Index) && loc.Path[2] == protoPath(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), "Field") && loc.Path[3] == int32(fieldIdx) { + if loc.LeadingComments != nil { + fieldDescription = strings.TrimRight(*loc.LeadingComments, "\n") + fieldDescription = strings.TrimLeft(fieldDescription, " ") + } + break + } + } + if primitive { // If repeated render as an array of items. if field.FieldDescriptorProto.GetLabel() == pbdescriptor.FieldDescriptorProto_LABEL_REPEATED { @@ -150,11 +164,13 @@ func renderMessagesAsDefinition(messages messageMap, d swaggerDefinitionsObject, Type: fieldType, Format: fieldFormat, }, + Description: fieldDescription, } } else { object.Properties[field.GetName()] = swaggerSchemaObject{ - Type: fieldType, - Format: fieldFormat, + Type: fieldType, + Format: fieldFormat, + Description: fieldDescription, } } } else { @@ -164,10 +180,12 @@ func renderMessagesAsDefinition(messages messageMap, d swaggerDefinitionsObject, Items: &swaggerItemsObject{ Ref: "#/definitions/" + fullyQualifiedNameToSwaggerName(field.GetTypeName(), reg), }, + Description: fieldDescription, } } else { object.Properties[field.GetName()] = swaggerSchemaObject{ - Ref: "#/definitions/" + fullyQualifiedNameToSwaggerName(field.GetTypeName(), reg), + Ref: "#/definitions/" + fullyQualifiedNameToSwaggerName(field.GetTypeName(), reg), + Description: fieldDescription, } } } @@ -418,24 +436,6 @@ func renderServices(services []*descriptor.Service, paths swaggerPathsObject, re pathItemObject = swaggerPathItemObject{} } - // TODO(ivucica): make this a module-level function and use elsewhere. - protoPath := func(descriptorType reflect.Type, what string) int32 { - // TODO(ivucica): handle errors obtaining any of the following. - field, ok := descriptorType.Elem().FieldByName(what) - if !ok { - // TODO(ivucica): consider being more graceful. - panic(fmt.Errorf("Could not find type id for %s.", what)) - } - pbtag := field.Tag.Get("protobuf") - if pbtag == "" { - // TODO(ivucica): consider being more graceful. - panic(fmt.Errorf("No protobuf tag on %s.", what)) - } - // TODO(ivucica): handle error - path, _ := strconv.Atoi(strings.Split(pbtag, ",")[1]) - - return int32(path) - } methDescription := "" for _, loc := range svc.File.SourceCodeInfo.Location { if len(loc.Path) < 4 { @@ -522,3 +522,21 @@ func applyTemplate(p param) (string, error) { return w.String(), nil } + +func protoPath(descriptorType reflect.Type, what string) int32 { + // TODO(ivucica): handle errors obtaining any of the following. + field, ok := descriptorType.Elem().FieldByName(what) + if !ok { + // TODO(ivucica): consider being more graceful. + panic(fmt.Errorf("Could not find type id for %s.", what)) + } + pbtag := field.Tag.Get("protobuf") + if pbtag == "" { + // TODO(ivucica): consider being more graceful. + panic(fmt.Errorf("No protobuf tag on %s.", what)) + } + // TODO(ivucica): handle error + path, _ := strconv.Atoi(strings.Split(pbtag, ",")[1]) + + return int32(path) +} diff --git a/protoc-gen-swagger/genswagger/types.go b/protoc-gen-swagger/genswagger/types.go index a3844420342..822470d2c4d 100644 --- a/protoc-gen-swagger/genswagger/types.go +++ b/protoc-gen-swagger/genswagger/types.go @@ -100,6 +100,8 @@ type swaggerSchemaObject struct { // start from 0 index it will be great. I don't think that is a good assumption. Enum []string `json:"enum,omitempty"` Default string `json:"default,omitempty"` + + Description string `json:"description,omitempty"` } // http://swagger.io/specification/#referenceObject From 97558d41854098b2e2d8c76ceb2e022b5fbd5c9c Mon Sep 17 00:00:00 2001 From: Yukinari Toyota Date: Wed, 27 Apr 2016 22:11:24 +0900 Subject: [PATCH 03/13] Generate Swagger description for enum types, enum values and nested messages using proto comments --- examples/examplepb/echo_service.swagger.json | 3 +- .../examplepb/streamless_everything.proto | 8 ++ .../streamless_everything.swagger.json | 12 +- .../descriptor/registry.go | 3 +- protoc-gen-grpc-gateway/descriptor/types.go | 2 + protoc-gen-swagger/genswagger/template.go | 118 +++++++++++++----- 6 files changed, 109 insertions(+), 37 deletions(-) diff --git a/examples/examplepb/echo_service.swagger.json b/examples/examplepb/echo_service.swagger.json index 6fc2437f051..84c0d128f4c 100644 --- a/examples/examplepb/echo_service.swagger.json +++ b/examples/examplepb/echo_service.swagger.json @@ -79,7 +79,8 @@ "format": "string", "description": "Id represents the message identifier." } - } + }, + "description": "SimpleMessage represents a simple message sent to the Echo service." } } } diff --git a/examples/examplepb/streamless_everything.proto b/examples/examplepb/streamless_everything.proto index a6d2145d8ab..6fc822915b3 100644 --- a/examples/examplepb/streamless_everything.proto +++ b/examples/examplepb/streamless_everything.proto @@ -6,11 +6,16 @@ import "google/api/annotations.proto"; import "examples/sub/message.proto"; message ABitOfEverything { + // Nested is nested type. message Nested { + // name is nested field. string name = 1; uint32 amount = 2; + // DeepEnum is one or zero. enum DeepEnum { + // FALSE is false. FALSE = 0; + // TRUE is true. TRUE = 1; } DeepEnum ok = 3; @@ -44,8 +49,11 @@ message IdMessage { string uuid = 1; } +// NumericEnum is one or zero. enum NumericEnum { + // ZERO means 0 ZERO = 0; + // ONE means 1 ONE = 1; } diff --git a/examples/examplepb/streamless_everything.swagger.json b/examples/examplepb/streamless_everything.swagger.json index 847e9e867df..b9130c8f11c 100644 --- a/examples/examplepb/streamless_everything.swagger.json +++ b/examples/examplepb/streamless_everything.swagger.json @@ -346,12 +346,14 @@ }, "name": { "type": "string", - "format": "string" + "format": "string", + "description": "name is nested field." }, "ok": { "$ref": "#/definitions/NestedDeepEnum" } - } + }, + "description": "Nested is nested type." }, "NestedDeepEnum": { "type": "string", @@ -359,7 +361,8 @@ "FALSE", "TRUE" ], - "default": "FALSE" + "default": "FALSE", + "description": "DeepEnum is one or zero. FALSE: FALSE is false. TRUE: TRUE is true." }, "examplepbABitOfEverything": { "properties": { @@ -457,7 +460,8 @@ "ZERO", "ONE" ], - "default": "ZERO" + "default": "ZERO", + "description": "NumericEnum is one or zero. ZERO: ZERO means 0 ONE: ONE means 1" }, "subStringMessage": { "properties": { diff --git a/protoc-gen-grpc-gateway/descriptor/registry.go b/protoc-gen-grpc-gateway/descriptor/registry.go index ee032f1edbb..f293f555824 100644 --- a/protoc-gen-grpc-gateway/descriptor/registry.go +++ b/protoc-gen-grpc-gateway/descriptor/registry.go @@ -125,11 +125,12 @@ func (r *Registry) registerMsg(file *File, outerPath []string, msgs []*descripto } func (r *Registry) registerEnum(file *File, outerPath []string, enums []*descriptor.EnumDescriptorProto) { - for _, ed := range enums { + for i, ed := range enums { e := &Enum{ File: file, Outers: outerPath, EnumDescriptorProto: ed, + Index: i, } file.Enums = append(file.Enums, e) r.enums[e.FQEN()] = e diff --git a/protoc-gen-grpc-gateway/descriptor/types.go b/protoc-gen-grpc-gateway/descriptor/types.go index cea2db84516..1a3caf96220 100644 --- a/protoc-gen-grpc-gateway/descriptor/types.go +++ b/protoc-gen-grpc-gateway/descriptor/types.go @@ -100,6 +100,8 @@ type Enum struct { // Outers is a list of outer messages if this enum is a nested type. Outers []string *descriptor.EnumDescriptorProto + + Index int } // FQEN returns a fully qualified enum name of this enum. diff --git a/protoc-gen-swagger/genswagger/template.go b/protoc-gen-swagger/genswagger/template.go index 8c2dd5fcb2c..f0b37850f1a 100644 --- a/protoc-gen-swagger/genswagger/template.go +++ b/protoc-gen-swagger/genswagger/template.go @@ -51,8 +51,10 @@ func findNestedMessagesAndEnumerations(message *descriptor.Message, reg *descrip func renderMessagesAsDefinition(messages messageMap, d swaggerDefinitionsObject, reg *descriptor.Registry) { for _, msg := range messages { + msgDescription := protoComments(reg, msg.File, msg.Outers, "MessageType", int32(msg.Index)) object := swaggerSchemaObject{ - Properties: map[string]swaggerSchemaObject{}, + Properties: map[string]swaggerSchemaObject{}, + Description: msgDescription, } for fieldIdx, field := range msg.Fields { var fieldType, fieldFormat string @@ -141,19 +143,8 @@ func renderMessagesAsDefinition(messages messageMap, d swaggerDefinitionsObject, fieldFormat = "UNKNOWN" } - fieldDescription := "" - for _, loc := range msg.File.SourceCodeInfo.Location { - if len(loc.Path) < 4 { - continue - } - if loc.Path[0] == protoPath(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "MessageType") && loc.Path[1] == int32(msg.Index) && loc.Path[2] == protoPath(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), "Field") && loc.Path[3] == int32(fieldIdx) { - if loc.LeadingComments != nil { - fieldDescription = strings.TrimRight(*loc.LeadingComments, "\n") - fieldDescription = strings.TrimLeft(fieldDescription, " ") - } - break - } - } + fieldProtoPath := protoPath(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), "Field") + fieldDescription := protoComments(reg, msg.File, msg.Outers, "MessageType", int32(msg.Index), fieldProtoPath, int32(fieldIdx)) if primitive { // If repeated render as an array of items. @@ -196,21 +187,30 @@ func renderMessagesAsDefinition(messages messageMap, d swaggerDefinitionsObject, // renderEnumerationsAsDefinition inserts enums into the definitions object. func renderEnumerationsAsDefinition(enums enumMap, d swaggerDefinitionsObject, reg *descriptor.Registry) { + valueProtoPath := protoPath(reflect.TypeOf((*pbdescriptor.EnumDescriptorProto)(nil)), "Value") for _, enum := range enums { + enumDescription := protoComments(reg, enum.File, enum.Outers, "EnumType", int32(enum.Index)) + var enumNames []string // it may be necessary to sort the result of the GetValue function. var defaultValue string - for _, value := range enum.GetValue() { + for valueIdx, value := range enum.GetValue() { enumNames = append(enumNames, value.GetName()) if defaultValue == "" && value.GetNumber() == 0 { defaultValue = value.GetName() } + + valueDescription := protoComments(reg, enum.File, enum.Outers, "EnumType", int32(enum.Index), valueProtoPath, int32(valueIdx)) + if valueDescription != "" { + enumDescription += " " + value.GetName() + ": " + valueDescription + } } d[fullyQualifiedNameToSwaggerName(enum.FQEN(), reg)] = swaggerSchemaObject{ - Type: "string", - Enum: enumNames, - Default: defaultValue, + Type: "string", + Enum: enumNames, + Default: defaultValue, + Description: enumDescription, } } } @@ -436,19 +436,8 @@ func renderServices(services []*descriptor.Service, paths swaggerPathsObject, re pathItemObject = swaggerPathItemObject{} } - methDescription := "" - for _, loc := range svc.File.SourceCodeInfo.Location { - if len(loc.Path) < 4 { - continue - } - if loc.Path[0] == protoPath(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "Service") && loc.Path[1] == int32(svcIdx) && loc.Path[2] == protoPath(reflect.TypeOf((*pbdescriptor.ServiceDescriptorProto)(nil)), "Method") && loc.Path[3] == int32(methIdx) { - if loc.LeadingComments != nil { - methDescription = strings.TrimRight(*loc.LeadingComments, "\n") - methDescription = strings.TrimLeft(methDescription, " ") - } - break - } - } + methProtoPath := protoPath(reflect.TypeOf((*pbdescriptor.ServiceDescriptorProto)(nil)), "Method") + methDescription := protoComments(reg, svc.File, nil, "Service", int32(svcIdx), methProtoPath, int32(methIdx)) operationObject := &swaggerOperationObject{ Summary: fmt.Sprintf("%s.%s", svc.GetName(), meth.GetName()), Description: methDescription, @@ -523,6 +512,73 @@ func applyTemplate(p param) (string, error) { return w.String(), nil } +func protoComments(reg *descriptor.Registry, file *descriptor.File, outers []string, typeName string, typeIndex int32, fieldPathes ...int32) string { + outerPathes := make([]int32, len(outers)) + for i := range outers { + location := "" + if file.Package != nil { + location = file.GetPackage() + } + + msg, err := reg.LookupMsg(location, strings.Join(outers[:i+1], ".")) + if err != nil { + panic(err) + } + outerPathes[i] = int32(msg.Index) + } + + messageProtoPath := protoPath(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "MessageType") + nestedProtoPath := protoPath(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), "NestedType") +L1: + for _, loc := range file.SourceCodeInfo.Location { + if len(loc.Path) < len(outerPathes)*2+2+len(fieldPathes) { + continue + } + + for i, v := range outerPathes { + if i == 0 && loc.Path[i*2+0] != messageProtoPath { + continue L1 + } + if i != 0 && loc.Path[i*2+0] != nestedProtoPath { + continue L1 + } + if loc.Path[i*2+1] != v { + continue L1 + } + } + + outerOffset := len(outerPathes) * 2 + if outerOffset == 0 && loc.Path[outerOffset] != protoPath(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), typeName) { + continue + } + if outerOffset != 0 { + if typeName == "MessageType" { + typeName = "NestedType" + } + if loc.Path[outerOffset] != protoPath(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), typeName) { + continue + } + } + if loc.Path[outerOffset+1] != typeIndex { + continue + } + + for i, v := range fieldPathes { + if loc.Path[outerOffset+2+i] != v { + continue L1 + } + } + + comments := "" + if loc.LeadingComments != nil { + comments = strings.TrimRight(*loc.LeadingComments, "\n") + comments = strings.TrimSpace(comments) + } + return comments + } + return "" +} + func protoPath(descriptorType reflect.Type, what string) int32 { // TODO(ivucica): handle errors obtaining any of the following. field, ok := descriptorType.Elem().FieldByName(what) From a2e787e03399a921e73fac9f7ca974865fdfa507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Vu=C4=8Dica?= Date: Sun, 1 May 2016 12:44:58 +0100 Subject: [PATCH 04/13] Documented protoPathIndex. Improved error handling. Also, fixed a few typos and renamed protoPath into protoPathIndex (as we are not returning the entire path in that method). --- examples/examplepb/a_bit_of_everything.pb.go | 98 +++++++++++++------ examples/examplepb/echo_service.pb.go | 40 +++++--- examples/examplepb/echo_service.swagger.json | 1 + examples/examplepb/flow_combination.pb.go | 66 +++++++++---- .../streamless_everything.swagger.json | 8 +- protoc-gen-swagger/genswagger/template.go | 67 ++++++++----- 6 files changed, 194 insertions(+), 86 deletions(-) diff --git a/examples/examplepb/a_bit_of_everything.pb.go b/examples/examplepb/a_bit_of_everything.pb.go index 18b174ac92f..21b8e96c92c 100644 --- a/examples/examplepb/a_bit_of_everything.pb.go +++ b/examples/examplepb/a_bit_of_everything.pb.go @@ -132,7 +132,7 @@ var _ grpc.ClientConn // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion1 +const _ = grpc.SupportPackageIsVersion2 // Client API for ABitOfEverythingService service @@ -325,28 +325,40 @@ func RegisterABitOfEverythingServiceServer(s *grpc.Server, srv ABitOfEverythingS s.RegisterService(&_ABitOfEverythingService_serviceDesc, srv) } -func _ABitOfEverythingService_Create_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { +func _ABitOfEverythingService_Create_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ABitOfEverything) if err := dec(in); err != nil { return nil, err } - out, err := srv.(ABitOfEverythingServiceServer).Create(ctx, in) - if err != nil { - return nil, err + if interceptor == nil { + return srv.(ABitOfEverythingServiceServer).Create(ctx, in) } - return out, nil + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gengo.grpc.gateway.examples.examplepb.ABitOfEverythingService/Create", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ABitOfEverythingServiceServer).Create(ctx, req.(*ABitOfEverything)) + } + return interceptor(ctx, in, info, handler) } -func _ABitOfEverythingService_CreateBody_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { +func _ABitOfEverythingService_CreateBody_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ABitOfEverything) if err := dec(in); err != nil { return nil, err } - out, err := srv.(ABitOfEverythingServiceServer).CreateBody(ctx, in) - if err != nil { - return nil, err + if interceptor == nil { + return srv.(ABitOfEverythingServiceServer).CreateBody(ctx, in) } - return out, nil + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gengo.grpc.gateway.examples.examplepb.ABitOfEverythingService/CreateBody", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ABitOfEverythingServiceServer).CreateBody(ctx, req.(*ABitOfEverything)) + } + return interceptor(ctx, in, info, handler) } func _ABitOfEverythingService_BulkCreate_Handler(srv interface{}, stream grpc.ServerStream) error { @@ -375,16 +387,22 @@ func (x *aBitOfEverythingServiceBulkCreateServer) Recv() (*ABitOfEverything, err return m, nil } -func _ABitOfEverythingService_Lookup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { +func _ABitOfEverythingService_Lookup_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(sub2.IdMessage) if err := dec(in); err != nil { return nil, err } - out, err := srv.(ABitOfEverythingServiceServer).Lookup(ctx, in) - if err != nil { - return nil, err + if interceptor == nil { + return srv.(ABitOfEverythingServiceServer).Lookup(ctx, in) } - return out, nil + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gengo.grpc.gateway.examples.examplepb.ABitOfEverythingService/Lookup", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ABitOfEverythingServiceServer).Lookup(ctx, req.(*sub2.IdMessage)) + } + return interceptor(ctx, in, info, handler) } func _ABitOfEverythingService_List_Handler(srv interface{}, stream grpc.ServerStream) error { @@ -408,40 +426,58 @@ func (x *aBitOfEverythingServiceListServer) Send(m *ABitOfEverything) error { return x.ServerStream.SendMsg(m) } -func _ABitOfEverythingService_Update_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { +func _ABitOfEverythingService_Update_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(ABitOfEverything) if err := dec(in); err != nil { return nil, err } - out, err := srv.(ABitOfEverythingServiceServer).Update(ctx, in) - if err != nil { - return nil, err + if interceptor == nil { + return srv.(ABitOfEverythingServiceServer).Update(ctx, in) } - return out, nil + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gengo.grpc.gateway.examples.examplepb.ABitOfEverythingService/Update", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ABitOfEverythingServiceServer).Update(ctx, req.(*ABitOfEverything)) + } + return interceptor(ctx, in, info, handler) } -func _ABitOfEverythingService_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { +func _ABitOfEverythingService_Delete_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(sub2.IdMessage) if err := dec(in); err != nil { return nil, err } - out, err := srv.(ABitOfEverythingServiceServer).Delete(ctx, in) - if err != nil { - return nil, err + if interceptor == nil { + return srv.(ABitOfEverythingServiceServer).Delete(ctx, in) } - return out, nil + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gengo.grpc.gateway.examples.examplepb.ABitOfEverythingService/Delete", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ABitOfEverythingServiceServer).Delete(ctx, req.(*sub2.IdMessage)) + } + return interceptor(ctx, in, info, handler) } -func _ABitOfEverythingService_Echo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { +func _ABitOfEverythingService_Echo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(gengo_grpc_gateway_examples_sub.StringMessage) if err := dec(in); err != nil { return nil, err } - out, err := srv.(ABitOfEverythingServiceServer).Echo(ctx, in) - if err != nil { - return nil, err + if interceptor == nil { + return srv.(ABitOfEverythingServiceServer).Echo(ctx, in) } - return out, nil + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gengo.grpc.gateway.examples.examplepb.ABitOfEverythingService/Echo", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ABitOfEverythingServiceServer).Echo(ctx, req.(*gengo_grpc_gateway_examples_sub.StringMessage)) + } + return interceptor(ctx, in, info, handler) } func _ABitOfEverythingService_BulkEcho_Handler(srv interface{}, stream grpc.ServerStream) error { diff --git a/examples/examplepb/echo_service.pb.go b/examples/examplepb/echo_service.pb.go index 76cd1d1250c..20274707d60 100644 --- a/examples/examplepb/echo_service.pb.go +++ b/examples/examplepb/echo_service.pb.go @@ -41,7 +41,9 @@ var _ = math.Inf // is compatible with the proto package it is being compiled against. const _ = proto.ProtoPackageIsVersion1 +// SimpleMessage represents a simple message sent to the Echo service. type SimpleMessage struct { + // Id represents the message identifier. Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"` } @@ -60,12 +62,14 @@ var _ grpc.ClientConn // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion1 +const _ = grpc.SupportPackageIsVersion2 // Client API for EchoService service type EchoServiceClient interface { + // Echo method receives a simple message and returns it. Echo(ctx context.Context, in *SimpleMessage, opts ...grpc.CallOption) (*SimpleMessage, error) + // EchoBody method receives a simple message and returns it. EchoBody(ctx context.Context, in *SimpleMessage, opts ...grpc.CallOption) (*SimpleMessage, error) } @@ -98,7 +102,9 @@ func (c *echoServiceClient) EchoBody(ctx context.Context, in *SimpleMessage, opt // Server API for EchoService service type EchoServiceServer interface { + // Echo method receives a simple message and returns it. Echo(context.Context, *SimpleMessage) (*SimpleMessage, error) + // EchoBody method receives a simple message and returns it. EchoBody(context.Context, *SimpleMessage) (*SimpleMessage, error) } @@ -106,28 +112,40 @@ func RegisterEchoServiceServer(s *grpc.Server, srv EchoServiceServer) { s.RegisterService(&_EchoService_serviceDesc, srv) } -func _EchoService_Echo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { +func _EchoService_Echo_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SimpleMessage) if err := dec(in); err != nil { return nil, err } - out, err := srv.(EchoServiceServer).Echo(ctx, in) - if err != nil { - return nil, err + if interceptor == nil { + return srv.(EchoServiceServer).Echo(ctx, in) } - return out, nil + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gengo.grpc.gateway.examples.examplepb.EchoService/Echo", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EchoServiceServer).Echo(ctx, req.(*SimpleMessage)) + } + return interceptor(ctx, in, info, handler) } -func _EchoService_EchoBody_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { +func _EchoService_EchoBody_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SimpleMessage) if err := dec(in); err != nil { return nil, err } - out, err := srv.(EchoServiceServer).EchoBody(ctx, in) - if err != nil { - return nil, err + if interceptor == nil { + return srv.(EchoServiceServer).EchoBody(ctx, in) } - return out, nil + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gengo.grpc.gateway.examples.examplepb.EchoService/EchoBody", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(EchoServiceServer).EchoBody(ctx, req.(*SimpleMessage)) + } + return interceptor(ctx, in, info, handler) } var _EchoService_serviceDesc = grpc.ServiceDesc{ diff --git a/examples/examplepb/echo_service.swagger.json b/examples/examplepb/echo_service.swagger.json index 84c0d128f4c..988ff327271 100644 --- a/examples/examplepb/echo_service.swagger.json +++ b/examples/examplepb/echo_service.swagger.json @@ -73,6 +73,7 @@ }, "definitions": { "examplepbSimpleMessage": { + "type": "object", "properties": { "id": { "type": "string", diff --git a/examples/examplepb/flow_combination.pb.go b/examples/examplepb/flow_combination.pb.go index 40606b6eb0d..345f7ed5a8a 100644 --- a/examples/examplepb/flow_combination.pb.go +++ b/examples/examplepb/flow_combination.pb.go @@ -95,7 +95,7 @@ var _ grpc.ClientConn // This is a compile-time assertion to ensure that this generated file // is compatible with the grpc package it is being compiled against. -const _ = grpc.SupportPackageIsVersion1 +const _ = grpc.SupportPackageIsVersion2 // Client API for FlowCombination service @@ -368,16 +368,22 @@ func RegisterFlowCombinationServer(s *grpc.Server, srv FlowCombinationServer) { s.RegisterService(&_FlowCombination_serviceDesc, srv) } -func _FlowCombination_RpcEmptyRpc_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { +func _FlowCombination_RpcEmptyRpc_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(EmptyProto) if err := dec(in); err != nil { return nil, err } - out, err := srv.(FlowCombinationServer).RpcEmptyRpc(ctx, in) - if err != nil { - return nil, err + if interceptor == nil { + return srv.(FlowCombinationServer).RpcEmptyRpc(ctx, in) } - return out, nil + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gengo.grpc.gateway.examples.examplepb.FlowCombination/RpcEmptyRpc", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(FlowCombinationServer).RpcEmptyRpc(ctx, req.(*EmptyProto)) + } + return interceptor(ctx, in, info, handler) } func _FlowCombination_RpcEmptyStream_Handler(srv interface{}, stream grpc.ServerStream) error { @@ -453,40 +459,58 @@ func (x *flowCombinationStreamEmptyStreamServer) Recv() (*EmptyProto, error) { return m, nil } -func _FlowCombination_RpcBodyRpc_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { +func _FlowCombination_RpcBodyRpc_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(NonEmptyProto) if err := dec(in); err != nil { return nil, err } - out, err := srv.(FlowCombinationServer).RpcBodyRpc(ctx, in) - if err != nil { - return nil, err + if interceptor == nil { + return srv.(FlowCombinationServer).RpcBodyRpc(ctx, in) } - return out, nil + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gengo.grpc.gateway.examples.examplepb.FlowCombination/RpcBodyRpc", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(FlowCombinationServer).RpcBodyRpc(ctx, req.(*NonEmptyProto)) + } + return interceptor(ctx, in, info, handler) } -func _FlowCombination_RpcPathSingleNestedRpc_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { +func _FlowCombination_RpcPathSingleNestedRpc_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(SingleNestedProto) if err := dec(in); err != nil { return nil, err } - out, err := srv.(FlowCombinationServer).RpcPathSingleNestedRpc(ctx, in) - if err != nil { - return nil, err + if interceptor == nil { + return srv.(FlowCombinationServer).RpcPathSingleNestedRpc(ctx, in) } - return out, nil + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gengo.grpc.gateway.examples.examplepb.FlowCombination/RpcPathSingleNestedRpc", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(FlowCombinationServer).RpcPathSingleNestedRpc(ctx, req.(*SingleNestedProto)) + } + return interceptor(ctx, in, info, handler) } -func _FlowCombination_RpcPathNestedRpc_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error) (interface{}, error) { +func _FlowCombination_RpcPathNestedRpc_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { in := new(NestedProto) if err := dec(in); err != nil { return nil, err } - out, err := srv.(FlowCombinationServer).RpcPathNestedRpc(ctx, in) - if err != nil { - return nil, err + if interceptor == nil { + return srv.(FlowCombinationServer).RpcPathNestedRpc(ctx, in) } - return out, nil + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/gengo.grpc.gateway.examples.examplepb.FlowCombination/RpcPathNestedRpc", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(FlowCombinationServer).RpcPathNestedRpc(ctx, req.(*NestedProto)) + } + return interceptor(ctx, in, info, handler) } func _FlowCombination_RpcBodyStream_Handler(srv interface{}, stream grpc.ServerStream) error { diff --git a/examples/examplepb/streamless_everything.swagger.json b/examples/examplepb/streamless_everything.swagger.json index b9130c8f11c..928a890c537 100644 --- a/examples/examplepb/streamless_everything.swagger.json +++ b/examples/examplepb/streamless_everything.swagger.json @@ -339,6 +339,7 @@ }, "definitions": { "ABitOfEverythingNested": { + "type": "object", "properties": { "amount": { "type": "integer", @@ -365,6 +366,7 @@ "description": "DeepEnum is one or zero. FALSE: FALSE is false. TRUE: TRUE is true." }, "examplepbABitOfEverything": { + "type": "object", "properties": { "bool_value": { "type": "boolean", @@ -445,8 +447,11 @@ } } }, - "examplepbEmptyMessage": {}, + "examplepbEmptyMessage": { + "type": "object" + }, "examplepbIdMessage": { + "type": "object", "properties": { "uuid": { "type": "string", @@ -464,6 +469,7 @@ "description": "NumericEnum is one or zero. ZERO: ZERO means 0 ONE: ONE means 1" }, "subStringMessage": { + "type": "object", "properties": { "value": { "type": "string", diff --git a/protoc-gen-swagger/genswagger/template.go b/protoc-gen-swagger/genswagger/template.go index f0b37850f1a..b18e44d109b 100644 --- a/protoc-gen-swagger/genswagger/template.go +++ b/protoc-gen-swagger/genswagger/template.go @@ -143,7 +143,7 @@ func renderMessagesAsDefinition(messages messageMap, d swaggerDefinitionsObject, fieldFormat = "UNKNOWN" } - fieldProtoPath := protoPath(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), "Field") + fieldProtoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), "Field") fieldDescription := protoComments(reg, msg.File, msg.Outers, "MessageType", int32(msg.Index), fieldProtoPath, int32(fieldIdx)) if primitive { @@ -187,7 +187,7 @@ func renderMessagesAsDefinition(messages messageMap, d swaggerDefinitionsObject, // renderEnumerationsAsDefinition inserts enums into the definitions object. func renderEnumerationsAsDefinition(enums enumMap, d swaggerDefinitionsObject, reg *descriptor.Registry) { - valueProtoPath := protoPath(reflect.TypeOf((*pbdescriptor.EnumDescriptorProto)(nil)), "Value") + valueProtoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.EnumDescriptorProto)(nil)), "Value") for _, enum := range enums { enumDescription := protoComments(reg, enum.File, enum.Outers, "EnumType", int32(enum.Index)) @@ -436,7 +436,7 @@ func renderServices(services []*descriptor.Service, paths swaggerPathsObject, re pathItemObject = swaggerPathItemObject{} } - methProtoPath := protoPath(reflect.TypeOf((*pbdescriptor.ServiceDescriptorProto)(nil)), "Method") + methProtoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.ServiceDescriptorProto)(nil)), "Method") methDescription := protoComments(reg, svc.File, nil, "Service", int32(svcIdx), methProtoPath, int32(methIdx)) operationObject := &swaggerOperationObject{ Summary: fmt.Sprintf("%s.%s", svc.GetName(), meth.GetName()), @@ -512,8 +512,8 @@ func applyTemplate(p param) (string, error) { return w.String(), nil } -func protoComments(reg *descriptor.Registry, file *descriptor.File, outers []string, typeName string, typeIndex int32, fieldPathes ...int32) string { - outerPathes := make([]int32, len(outers)) +func protoComments(reg *descriptor.Registry, file *descriptor.File, outers []string, typeName string, typeIndex int32, fieldPaths ...int32) string { + outerPaths := make([]int32, len(outers)) for i := range outers { location := "" if file.Package != nil { @@ -524,18 +524,18 @@ func protoComments(reg *descriptor.Registry, file *descriptor.File, outers []str if err != nil { panic(err) } - outerPathes[i] = int32(msg.Index) + outerPaths[i] = int32(msg.Index) } - messageProtoPath := protoPath(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "MessageType") - nestedProtoPath := protoPath(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), "NestedType") + messageProtoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "MessageType") + nestedProtoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), "NestedType") L1: for _, loc := range file.SourceCodeInfo.Location { - if len(loc.Path) < len(outerPathes)*2+2+len(fieldPathes) { + if len(loc.Path) < len(outerPaths)*2+2+len(fieldPaths) { continue } - for i, v := range outerPathes { + for i, v := range outerPaths { if i == 0 && loc.Path[i*2+0] != messageProtoPath { continue L1 } @@ -547,15 +547,15 @@ L1: } } - outerOffset := len(outerPathes) * 2 - if outerOffset == 0 && loc.Path[outerOffset] != protoPath(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), typeName) { + outerOffset := len(outerPaths) * 2 + if outerOffset == 0 && loc.Path[outerOffset] != protoPathIndex(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), typeName) { continue } if outerOffset != 0 { if typeName == "MessageType" { typeName = "NestedType" } - if loc.Path[outerOffset] != protoPath(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), typeName) { + if loc.Path[outerOffset] != protoPathIndex(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), typeName) { continue } } @@ -563,7 +563,7 @@ L1: continue } - for i, v := range fieldPathes { + for i, v := range fieldPaths { if loc.Path[outerOffset+2+i] != v { continue L1 } @@ -579,20 +579,43 @@ L1: return "" } -func protoPath(descriptorType reflect.Type, what string) int32 { - // TODO(ivucica): handle errors obtaining any of the following. +// protoPathIndex returns a path component for google.protobuf.descriptor.SourceCode_Location. +// +// Specifically, it returns an id as generated from descriptor proto which +// can be used to determine what type the id following it in the path is. +// For example, if we are trying to locate comments related to a field named +// `Address` in a message named `Person`, the path will be: +// +// [4, a, 2, b] +// +// While `a` gets determined by the order in which the messages appear in +// the proto file, and `b` is the field index specified in the proto +// file itself, the path actually needs to specify that `a` refers to a +// message and not, say, a service; and that `b` refers to a field and not +// an option. +// +// protoPathIndex figures out the values 4 and 2 in the above example. Because +// messages are top level objects, the value of 4 comes from field id for +// `MessageType` inside `google.protobuf.descriptor.FileDescriptor` message. +// This field has a message type `google.protobuf.descriptor.DescriptorProto`. +// And inside message `DescriptorProto`, there is a field named `Field` with id +// 2. +// +// Some code generators seem to be hardcoding these values; this method instead +// interprets them from `descriptor.proto`-derived Go source as necessary. +func protoPathIndex(descriptorType reflect.Type, what string) int32 { field, ok := descriptorType.Elem().FieldByName(what) if !ok { - // TODO(ivucica): consider being more graceful. - panic(fmt.Errorf("Could not find type id for %s.", what)) + panic(fmt.Errorf("Could not find protobuf descriptor type id for %s.", what)) } pbtag := field.Tag.Get("protobuf") if pbtag == "" { - // TODO(ivucica): consider being more graceful. - panic(fmt.Errorf("No protobuf tag on %s.", what)) + panic(fmt.Errorf("No Go tag 'protobuf' on protobuf descriptor for %s.", what)) + } + path, err := strconv.Atoi(strings.Split(pbtag, ",")[1]) + if err != nil { + panic(fmt.Errorf("Protobuf descriptor id for %s cannot be converted to a number: %s", what, err.Error())) } - // TODO(ivucica): handle error - path, _ := strconv.Atoi(strings.Split(pbtag, ",")[1]) return int32(path) } From 1ac3f80c6c0ad6a9ba8f4e063b549ef112044ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Vu=C4=8Dica?= Date: Sun, 1 May 2016 22:15:19 +0100 Subject: [PATCH 05/13] Fix test which was not setting the SourceCodeInfo field. This field should generally be non-nil while running the generator. This commit also adds a trivial check to make panic more informative in case a future test breaks in a similar fashion. --- protoc-gen-swagger/genswagger/template.go | 10 +++++++ .../genswagger/template_test.go | 29 ++++++++++--------- 2 files changed, 26 insertions(+), 13 deletions(-) diff --git a/protoc-gen-swagger/genswagger/template.go b/protoc-gen-swagger/genswagger/template.go index 525fcc01a97..f6290f48402 100644 --- a/protoc-gen-swagger/genswagger/template.go +++ b/protoc-gen-swagger/genswagger/template.go @@ -514,6 +514,16 @@ func applyTemplate(p param) (string, error) { } func protoComments(reg *descriptor.Registry, file *descriptor.File, outers []string, typeName string, typeIndex int32, fieldPaths ...int32) string { + if file.SourceCodeInfo == nil { + // Curious! A file without any source code info. + // This could be a test that's providing incomplete + // descriptor.File information. + // + // We could simply return no comments, but panic + // could make debugging easier. + panic("descriptor.File should not contain nil SourceCodeInfo") + } + outerPaths := make([]int32, len(outers)) for i := range outers { location := "" diff --git a/protoc-gen-swagger/genswagger/template_test.go b/protoc-gen-swagger/genswagger/template_test.go index c27812acd85..270b38d3560 100644 --- a/protoc-gen-swagger/genswagger/template_test.go +++ b/protoc-gen-swagger/genswagger/template_test.go @@ -48,11 +48,12 @@ func TestApplyTemplateSimple(t *testing.T) { } file := descriptor.File{ FileDescriptorProto: &protodescriptor.FileDescriptorProto{ - Name: proto.String("example.proto"), - Package: proto.String("example"), - Dependency: []string{"a.example/b/c.proto", "a.example/d/e.proto"}, - MessageType: []*protodescriptor.DescriptorProto{msgdesc}, - Service: []*protodescriptor.ServiceDescriptorProto{svc}, + SourceCodeInfo: &protodescriptor.SourceCodeInfo{}, + Name: proto.String("example.proto"), + Package: proto.String("example"), + Dependency: []string{"a.example/b/c.proto", "a.example/d/e.proto"}, + MessageType: []*protodescriptor.DescriptorProto{msgdesc}, + Service: []*protodescriptor.ServiceDescriptorProto{svc}, }, GoPkg: descriptor.GoPackage{ Path: "example.com/path/to/example/example.pb", @@ -182,10 +183,11 @@ func TestApplyTemplateRequestWithoutClientStreaming(t *testing.T) { } file := descriptor.File{ FileDescriptorProto: &protodescriptor.FileDescriptorProto{ - Name: proto.String("example.proto"), - Package: proto.String("example"), - MessageType: []*protodescriptor.DescriptorProto{msgdesc, nesteddesc}, - Service: []*protodescriptor.ServiceDescriptorProto{svc}, + SourceCodeInfo: &protodescriptor.SourceCodeInfo{}, + Name: proto.String("example.proto"), + Package: proto.String("example"), + MessageType: []*protodescriptor.DescriptorProto{msgdesc, nesteddesc}, + Service: []*protodescriptor.ServiceDescriptorProto{svc}, }, GoPkg: descriptor.GoPackage{ Path: "example.com/path/to/example/example.pb", @@ -343,10 +345,11 @@ func TestApplyTemplateRequestWithClientStreaming(t *testing.T) { } file := descriptor.File{ FileDescriptorProto: &protodescriptor.FileDescriptorProto{ - Name: proto.String("example.proto"), - Package: proto.String("example"), - MessageType: []*protodescriptor.DescriptorProto{msgdesc, nesteddesc}, - Service: []*protodescriptor.ServiceDescriptorProto{svc}, + SourceCodeInfo: &protodescriptor.SourceCodeInfo{}, + Name: proto.String("example.proto"), + Package: proto.String("example"), + MessageType: []*protodescriptor.DescriptorProto{msgdesc, nesteddesc}, + Service: []*protodescriptor.ServiceDescriptorProto{svc}, }, GoPkg: descriptor.GoPackage{ Path: "example.com/path/to/example/example.pb", From 246704b074784407232c38fb9d7da8992045fe9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Vu=C4=8Dica?= Date: Sat, 7 May 2016 20:01:42 +0100 Subject: [PATCH 06/13] Swagger: summary, description, custom JSON, package docs. This patch will extract summary separately from description in Swagger objects that happen to support both summary and description. It will do so by splitting the relevant proto comment based on paragraphs, and using the first paragraph as the summary. This patch will also allow updating the Swagger schema's otherwise hard-to-map fields by allowing custom JSON to be placed within the proto comment. The syntax leaves something to be desired, but it works. Use of JSON is, on the other hand, rather universal. This patch will also allow describing the whole API by attaching the comments to the 'package' stanza inside the proto. In this case, summary will be applied to info.title, and description will be applied to info.description. Other properties have to be set through JSON. Schema has been slightly updated to allow for fields such as termsOfService, contact, license, externalDocs, etc. If a method is not documented, stub summary for Swagger Operation is no longer generated. Documentation for enum values is now multiline with dashes before values. --- examples/examplepb/echo_service.pb.go | 57 +++++ examples/examplepb/echo_service.proto | 47 +++- examples/examplepb/echo_service.swagger.json | 24 +- .../streamless_everything.swagger.json | 17 +- protoc-gen-swagger/genswagger/template.go | 241 ++++++++++++++---- protoc-gen-swagger/genswagger/types.go | 33 ++- 6 files changed, 340 insertions(+), 79 deletions(-) diff --git a/examples/examplepb/echo_service.pb.go b/examples/examplepb/echo_service.pb.go index 20274707d60..d191846eaec 100644 --- a/examples/examplepb/echo_service.pb.go +++ b/examples/examplepb/echo_service.pb.go @@ -5,6 +5,30 @@ /* Package examplepb is a generated protocol buffer package. +Echo Service + +Echo Service API consists of a single service which returns +a message. + + + It is generated from these files: examples/examplepb/echo_service.proto examples/examplepb/a_bit_of_everything.proto @@ -42,6 +66,15 @@ var _ = math.Inf const _ = proto.ProtoPackageIsVersion1 // SimpleMessage represents a simple message sent to the Echo service. +// +// type SimpleMessage struct { // Id represents the message identifier. Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"` @@ -68,6 +101,18 @@ const _ = grpc.SupportPackageIsVersion2 type EchoServiceClient interface { // Echo method receives a simple message and returns it. + // + // The message posted as the id parameter will also be + // returned. + // + // Echo(ctx context.Context, in *SimpleMessage, opts ...grpc.CallOption) (*SimpleMessage, error) // EchoBody method receives a simple message and returns it. EchoBody(ctx context.Context, in *SimpleMessage, opts ...grpc.CallOption) (*SimpleMessage, error) @@ -103,6 +148,18 @@ func (c *echoServiceClient) EchoBody(ctx context.Context, in *SimpleMessage, opt type EchoServiceServer interface { // Echo method receives a simple message and returns it. + // + // The message posted as the id parameter will also be + // returned. + // + // Echo(context.Context, *SimpleMessage) (*SimpleMessage, error) // EchoBody method receives a simple message and returns it. EchoBody(context.Context, *SimpleMessage) (*SimpleMessage, error) diff --git a/examples/examplepb/echo_service.proto b/examples/examplepb/echo_service.proto index 83ebe704ed3..4baeddad06c 100644 --- a/examples/examplepb/echo_service.proto +++ b/examples/examplepb/echo_service.proto @@ -1,10 +1,43 @@ syntax = "proto3"; option go_package = "examplepb"; + +// Echo Service +// +// Echo Service API consists of a single service which returns +// a message. +// +// package gengo.grpc.gateway.examples.examplepb; import "google/api/annotations.proto"; -// SimpleMessage represents a simple message sent to the Echo service. +// SimpleMessage represents a simple message sent to the Echo service. +// +// message SimpleMessage { // Id represents the message identifier. string id = 1; @@ -13,6 +46,18 @@ message SimpleMessage { // Echo service responds to incoming echo requests. service EchoService { // Echo method receives a simple message and returns it. + // + // The message posted as the id parameter will also be + // returned. + // + // rpc Echo(SimpleMessage) returns (SimpleMessage) { option (google.api.http) = { post: "/v1/example/echo/{id}" diff --git a/examples/examplepb/echo_service.swagger.json b/examples/examplepb/echo_service.swagger.json index 988ff327271..39f226e1b09 100644 --- a/examples/examplepb/echo_service.swagger.json +++ b/examples/examplepb/echo_service.swagger.json @@ -1,9 +1,16 @@ { "swagger": "2.0", "info": { - "version": "", - "title": "" + "title": "Echo Service", + "description": "Echo Service API consists of a single service which returns\na message.", + "version": "1.0", + "contact": { + "name": "gRPC-Gateway project", + "url": "https://github.com/gengo/grpc-gateway", + "email": "none@example.com" + } }, + "host": "localhost", "schemes": [ "http", "https" @@ -17,8 +24,8 @@ "paths": { "/v1/example/echo/{id}": { "post": { - "summary": "EchoService.Echo", - "description": "Echo method receives a simple message and returns it.", + "summary": "Echo method receives a simple message and returns it.", + "description": "The message posted as the id parameter will also be\nreturned.", "operationId": "Echo", "responses": { "default": { @@ -39,13 +46,16 @@ ], "tags": [ "EchoService" - ] + ], + "externalDocs": { + "description": "Find out more about EchoService", + "url": "http://github.com/gengo/grpc-gateway" + } } }, "/v1/example/echo_body": { "post": { - "summary": "EchoService.EchoBody", - "description": "EchoBody method receives a simple message and returns it.", + "summary": "EchoBody method receives a simple message and returns it.", "operationId": "EchoBody", "responses": { "default": { diff --git a/examples/examplepb/streamless_everything.swagger.json b/examples/examplepb/streamless_everything.swagger.json index 928a890c537..bd314024891 100644 --- a/examples/examplepb/streamless_everything.swagger.json +++ b/examples/examplepb/streamless_everything.swagger.json @@ -1,8 +1,8 @@ { "swagger": "2.0", "info": { - "version": "", - "title": "" + "title": "examples/examplepb/streamless_everything.proto", + "version": "version not set" }, "schemes": [ "http", @@ -17,7 +17,6 @@ "paths": { "/v1/example/a_bit_of_everything": { "get": { - "summary": "ABitOfEverythingService.List", "operationId": "List", "responses": { "default": { @@ -32,7 +31,6 @@ ] }, "post": { - "summary": "ABitOfEverythingService.CreateBody", "operationId": "CreateBody", "responses": { "default": { @@ -59,7 +57,6 @@ }, "/v1/example/a_bit_of_everything/bulk": { "post": { - "summary": "ABitOfEverythingService.BulkCreate", "operationId": "BulkCreate", "responses": { "default": { @@ -86,7 +83,6 @@ }, "/v1/example/a_bit_of_everything/echo": { "post": { - "summary": "ABitOfEverythingService.BulkEcho", "operationId": "BulkEcho", "responses": { "default": { @@ -113,7 +109,6 @@ }, "/v1/example/a_bit_of_everything/echo/{value}": { "get": { - "summary": "ABitOfEverythingService.Echo", "operationId": "Echo", "responses": { "default": { @@ -139,7 +134,6 @@ }, "/v1/example/a_bit_of_everything/{float_value}/{double_value}/{int64_value}/separator/{uint64_value}/{int32_value}/{fixed64_value}/{fixed32_value}/{bool_value}/{string_value}/{uint32_value}/{sfixed32_value}/{sfixed64_value}/{sint32_value}/{sint64_value}": { "post": { - "summary": "ABitOfEverythingService.Create", "operationId": "Create", "responses": { "default": { @@ -256,7 +250,6 @@ }, "/v1/example/a_bit_of_everything/{uuid}": { "get": { - "summary": "ABitOfEverythingService.Lookup", "operationId": "Lookup", "responses": { "default": { @@ -280,7 +273,6 @@ ] }, "delete": { - "summary": "ABitOfEverythingService.Delete", "operationId": "Delete", "responses": { "default": { @@ -304,7 +296,6 @@ ] }, "put": { - "summary": "ABitOfEverythingService.Update", "operationId": "Update", "responses": { "default": { @@ -363,7 +354,7 @@ "TRUE" ], "default": "FALSE", - "description": "DeepEnum is one or zero. FALSE: FALSE is false. TRUE: TRUE is true." + "description": "DeepEnum is one or zero.\n\n - FALSE: FALSE is false.\n - TRUE: TRUE is true." }, "examplepbABitOfEverything": { "type": "object", @@ -466,7 +457,7 @@ "ONE" ], "default": "ZERO", - "description": "NumericEnum is one or zero. ZERO: ZERO means 0 ONE: ONE means 1" + "description": "NumericEnum is one or zero.\n\n - ZERO: ZERO means 0\n - ONE: ONE means 1" }, "subStringMessage": { "type": "object", diff --git a/protoc-gen-swagger/genswagger/template.go b/protoc-gen-swagger/genswagger/template.go index f6290f48402..96239a9b550 100644 --- a/protoc-gen-swagger/genswagger/template.go +++ b/protoc-gen-swagger/genswagger/template.go @@ -13,6 +13,8 @@ import ( pbdescriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" ) +var swaggerExtrasRegexp = regexp.MustCompile(`(?s)^(.*[^\s])[\s]*[\s]*(.*)$`) + // findServicesMessagesAndEnumerations discovers all messages and enums defined in the RPC methods of the service. func findServicesMessagesAndEnumerations(s []*descriptor.Service, reg *descriptor.Registry, m messageMap, e enumMap) { for _, svc := range s { @@ -51,11 +53,13 @@ func findNestedMessagesAndEnumerations(message *descriptor.Message, reg *descrip func renderMessagesAsDefinition(messages messageMap, d swaggerDefinitionsObject, reg *descriptor.Registry) { for _, msg := range messages { - msgDescription := protoComments(reg, msg.File, msg.Outers, "MessageType", int32(msg.Index)) object := swaggerSchemaObject{ - Type: "object", - Properties: map[string]swaggerSchemaObject{}, - Description: msgDescription, + Type: "object", + Properties: map[string]swaggerSchemaObject{}, + } + msgComments := protoComments(reg, msg.File, msg.Outers, "MessageType", int32(msg.Index)) + if err := updateSwaggerDataFromComments(&object, msgComments); err != nil { + panic(err) } for fieldIdx, field := range msg.Fields { var fieldType, fieldFormat string @@ -145,42 +149,43 @@ func renderMessagesAsDefinition(messages messageMap, d swaggerDefinitionsObject, } fieldProtoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), "Field") - fieldDescription := protoComments(reg, msg.File, msg.Outers, "MessageType", int32(msg.Index), fieldProtoPath, int32(fieldIdx)) + fieldProtoComments := protoComments(reg, msg.File, msg.Outers, "MessageType", int32(msg.Index), fieldProtoPath, int32(fieldIdx)) + var fieldValue swaggerSchemaObject if primitive { // If repeated render as an array of items. if field.FieldDescriptorProto.GetLabel() == pbdescriptor.FieldDescriptorProto_LABEL_REPEATED { - object.Properties[field.GetName()] = swaggerSchemaObject{ + fieldValue = swaggerSchemaObject{ Type: "array", Items: &swaggerItemsObject{ Type: fieldType, Format: fieldFormat, }, - Description: fieldDescription, } } else { - object.Properties[field.GetName()] = swaggerSchemaObject{ - Type: fieldType, - Format: fieldFormat, - Description: fieldDescription, + fieldValue = swaggerSchemaObject{ + Type: fieldType, + Format: fieldFormat, } } } else { if field.FieldDescriptorProto.GetLabel() == pbdescriptor.FieldDescriptorProto_LABEL_REPEATED { - object.Properties[field.GetName()] = swaggerSchemaObject{ + fieldValue = swaggerSchemaObject{ Type: "array", Items: &swaggerItemsObject{ Ref: "#/definitions/" + fullyQualifiedNameToSwaggerName(field.GetTypeName(), reg), }, - Description: fieldDescription, } } else { - object.Properties[field.GetName()] = swaggerSchemaObject{ - Ref: "#/definitions/" + fullyQualifiedNameToSwaggerName(field.GetTypeName(), reg), - Description: fieldDescription, + fieldValue = swaggerSchemaObject{ + Ref: "#/definitions/" + fullyQualifiedNameToSwaggerName(field.GetTypeName(), reg), } } } + if err := updateSwaggerDataFromComments(&fieldValue, fieldProtoComments); err != nil { + panic(err) + } + object.Properties[field.GetName()] = fieldValue } d[fullyQualifiedNameToSwaggerName(msg.FQMN(), reg)] = object } @@ -190,11 +195,12 @@ func renderMessagesAsDefinition(messages messageMap, d swaggerDefinitionsObject, func renderEnumerationsAsDefinition(enums enumMap, d swaggerDefinitionsObject, reg *descriptor.Registry) { valueProtoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.EnumDescriptorProto)(nil)), "Value") for _, enum := range enums { - enumDescription := protoComments(reg, enum.File, enum.Outers, "EnumType", int32(enum.Index)) + enumComments := protoComments(reg, enum.File, enum.Outers, "EnumType", int32(enum.Index)) var enumNames []string // it may be necessary to sort the result of the GetValue function. var defaultValue string + var valueDescriptions []string for valueIdx, value := range enum.GetValue() { enumNames = append(enumNames, value.GetName()) if defaultValue == "" && value.GetNumber() == 0 { @@ -203,16 +209,23 @@ func renderEnumerationsAsDefinition(enums enumMap, d swaggerDefinitionsObject, r valueDescription := protoComments(reg, enum.File, enum.Outers, "EnumType", int32(enum.Index), valueProtoPath, int32(valueIdx)) if valueDescription != "" { - enumDescription += " " + value.GetName() + ": " + valueDescription + valueDescriptions = append(valueDescriptions, value.GetName()+": "+valueDescription) } } - d[fullyQualifiedNameToSwaggerName(enum.FQEN(), reg)] = swaggerSchemaObject{ - Type: "string", - Enum: enumNames, - Default: defaultValue, - Description: enumDescription, + if len(valueDescriptions) > 0 { + enumComments += "\n\n - " + strings.Join(valueDescriptions, "\n - ") } + enumSchemaObject := swaggerSchemaObject{ + Type: "string", + Enum: enumNames, + Default: defaultValue, + } + if err := updateSwaggerDataFromComments(&enumSchemaObject, enumComments); err != nil { + panic(err) + } + + d[fullyQualifiedNameToSwaggerName(enum.FQEN(), reg)] = enumSchemaObject } } @@ -438,10 +451,7 @@ func renderServices(services []*descriptor.Service, paths swaggerPathsObject, re } methProtoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.ServiceDescriptorProto)(nil)), "Method") - methDescription := protoComments(reg, svc.File, nil, "Service", int32(svcIdx), methProtoPath, int32(methIdx)) operationObject := &swaggerOperationObject{ - Summary: fmt.Sprintf("%s.%s", svc.GetName(), meth.GetName()), - Description: methDescription, Tags: []string{svc.GetName()}, OperationId: fmt.Sprintf("%s", meth.GetName()), Parameters: parameters, @@ -454,6 +464,10 @@ func renderServices(services []*descriptor.Service, paths swaggerPathsObject, re }, }, } + methComments := protoComments(reg, svc.File, nil, "Service", int32(svcIdx), methProtoPath, int32(methIdx)) + if err := updateSwaggerDataFromComments(operationObject, methComments); err != nil { + panic(err) + } switch b.HTTPMethod { case "DELETE": @@ -490,6 +504,10 @@ func applyTemplate(p param) (string, error) { Produces: []string{"application/json"}, Paths: make(swaggerPathsObject), Definitions: make(swaggerDefinitionsObject), + Info: swaggerInfoObject{ + Title: *p.File.Name, + Version: "version not set", + }, } // Loops through all the services and their exposed GET/POST/PUT/DELETE definitions @@ -504,6 +522,13 @@ func applyTemplate(p param) (string, error) { renderMessagesAsDefinition(m, s.Definitions, p.reg) renderEnumerationsAsDefinition(e, s.Definitions, p.reg) + // File itself might have some comments and metadata. + packageProtoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "Package") + packageComments := protoComments(p.reg, p.File, nil, "Package", packageProtoPath) + if err := updateSwaggerDataFromComments(&s, packageComments); err != nil { + panic(err) + } + // We now have rendered the entire swagger object. Write the bytes out to a // string so it can be written to disk. var w bytes.Buffer @@ -513,6 +538,98 @@ func applyTemplate(p param) (string, error) { return w.String(), nil } +// updateSwaggerDataFromComments updates a Swagger object based on a comment +// from the proto file. +// +// As a first step, a section matching: +// +// +// +// where .* contains valid JSON will be stored for later processing, and then +// removed from the passed string. +// (Implementation note: Currently, the JSON gets immediately applied and +// thus cannot override summary and description.) +// +// First paragraph of a comment is used for summary. Remaining paragraphs of a +// comment are used for description. If 'Summary' field is not present on the +// passed swaggerObject, the summary and description are joined by \n\n. +// +// If there is a field named 'Info', its 'Summary' and 'Description' fields +// will be updated instead. (JSON always gets applied directly to the passed +// object.) +// +// If there is no 'Summary', the same behavior will be attempted on 'Title'. +// +// To apply additional Swagger properties, one can pass valid JSON as described +// before. This JSON gets parsed and applied to the passed swaggerObject +// directly. This lets users easily apply custom properties such as contact +// details, API base path, et al. +func updateSwaggerDataFromComments(swaggerObject interface{}, comment string) error { + // Find a section containing additional Swagger metadata. + matches := swaggerExtrasRegexp.FindStringSubmatch(comment) + + if len(matches) > 0 { + // If found, before further processing, replace the + // comment with a version that does not contain the + // extras. + comment = matches[1] + if len(matches[3]) > 0 { + comment += "\n\n" + matches[3] + } + + // Parse the JSON and apply it. + // TODO(ivucica): apply extras /after/ applying summary + // and description. + if err := json.Unmarshal([]byte(matches[2]), swaggerObject); err != nil { + return fmt.Errorf("error: %s, parsing: %s", err.Error(), matches[2]) + } + } + + // Figure out what to apply changes to. + swaggerObjectValue := reflect.ValueOf(swaggerObject) + infoObjectValue := swaggerObjectValue.Elem().FieldByName("Info") + if !infoObjectValue.CanSet() { + // No such field? Apply summary and description directly to + // passed object. + infoObjectValue = swaggerObjectValue.Elem() + } + + // Figure out which properties to update. + summaryValue := infoObjectValue.FieldByName("Summary") + descriptionValue := infoObjectValue.FieldByName("Description") + if !summaryValue.CanSet() { + summaryValue = infoObjectValue.FieldByName("Title") + } + + // If there is a summary (or summary-equivalent), use the first + // paragraph as summary, and the rest as description. + if summaryValue.CanSet() { + paragraphs := strings.Split(comment, "\n\n") + + summary := strings.TrimSpace(paragraphs[0]) + description := strings.TrimSpace(strings.Join(paragraphs[1:], "\n\n")) + if len(summary) > 0 { + summaryValue.Set(reflect.ValueOf(summary)) + } + if len(description) > 0 { + if !descriptionValue.CanSet() { + return fmt.Errorf("Object that has Summary but no Description?") + } + descriptionValue.Set(reflect.ValueOf(description)) + } + return nil + } + + // There was no summary field on the swaggerObject. Try to apply the + // whole comment into description. + if descriptionValue.CanSet() { + descriptionValue.Set(reflect.ValueOf(comment)) + return nil + } + + return fmt.Errorf("No description nor summary property.") +} + func protoComments(reg *descriptor.Registry, file *descriptor.File, outers []string, typeName string, typeIndex int32, fieldPaths ...int32) string { if file.SourceCodeInfo == nil { // Curious! A file without any source code info. @@ -540,50 +657,64 @@ func protoComments(reg *descriptor.Registry, file *descriptor.File, outers []str messageProtoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "MessageType") nestedProtoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), "NestedType") + packageProtoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "Package") L1: for _, loc := range file.SourceCodeInfo.Location { - if len(loc.Path) < len(outerPaths)*2+2+len(fieldPaths) { - continue - } - - for i, v := range outerPaths { - if i == 0 && loc.Path[i*2+0] != messageProtoPath { - continue L1 - } - if i != 0 && loc.Path[i*2+0] != nestedProtoPath { - continue L1 + if typeIndex != packageProtoPath { + if len(loc.Path) < len(outerPaths)*2+2+len(fieldPaths) { + continue } - if loc.Path[i*2+1] != v { - continue L1 + for i, v := range outerPaths { + if i == 0 && loc.Path[i*2+0] != messageProtoPath { + continue L1 + } + if i != 0 && loc.Path[i*2+0] != nestedProtoPath { + continue L1 + } + if loc.Path[i*2+1] != v { + continue L1 + } } - } - outerOffset := len(outerPaths) * 2 - if outerOffset == 0 && loc.Path[outerOffset] != protoPathIndex(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), typeName) { - continue - } - if outerOffset != 0 { - if typeName == "MessageType" { - typeName = "NestedType" + outerOffset := len(outerPaths) * 2 + if outerOffset == 0 && loc.Path[outerOffset] != protoPathIndex(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), typeName) { + continue + } + if outerOffset != 0 { + if typeName == "MessageType" { + typeName = "NestedType" + } + if loc.Path[outerOffset] != protoPathIndex(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), typeName) { + continue + } } - if loc.Path[outerOffset] != protoPathIndex(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), typeName) { + if loc.Path[outerOffset+1] != typeIndex { continue } - } - if loc.Path[outerOffset+1] != typeIndex { - continue - } - for i, v := range fieldPaths { - if loc.Path[outerOffset+2+i] != v { - continue L1 + for i, v := range fieldPaths { + if loc.Path[outerOffset+2+i] != v { + continue L1 + } + } + } else { + // path for package comments is just [2], and all the other processing + // is too complex for it. + if len(loc.Path) == 0 || typeIndex != loc.Path[0] { + continue } } - comments := "" if loc.LeadingComments != nil { comments = strings.TrimRight(*loc.LeadingComments, "\n") comments = strings.TrimSpace(comments) + // TODO(ivucica): this is a hack to fix "// " being interpreted as "//". + // perhaps we should: + // - split by \n + // - determine if every (but first and last) line begins with " " + // - trim every line only if that is the case + // - join by \n + comments = strings.Replace(comments, "\n ", "\n", -1) } return comments } diff --git a/protoc-gen-swagger/genswagger/types.go b/protoc-gen-swagger/genswagger/types.go index 822470d2c4d..e2ed057f7a7 100644 --- a/protoc-gen-swagger/genswagger/types.go +++ b/protoc-gen-swagger/genswagger/types.go @@ -15,8 +15,33 @@ type binding struct { // http://swagger.io/specification/#infoObject type swaggerInfoObject struct { - Version string `json:"version"` - Title string `json:"title"` + Title string `json:"title"` + Description string `json:"description,omitempty"` + TermsOfService string `json:"termsOfService,omitempty"` + Version string `json:"version"` + + Contact *swaggerContactObject `json:"contact,omitempty"` + License *swaggerLicenseObject `json:"license,omitempty"` + ExternalDocs *swaggerExternalDocumentationObject `json:"externalDocs,omitempty"` +} + +// http://swagger.io/specification/#contactObject +type swaggerContactObject struct { + Name string `json:"name,omitempty"` + URL string `json:"url,omitempty"` + Email string `json:"email,omitempty"` +} + +// http://swagger.io/specification/#licenseObject +type swaggerLicenseObject struct { + Name string `json:"name,omitempty"` + URL string `json:"url,omitempty"` +} + +// http://swagger.io/specification/#externalDocumentationObject +type swaggerExternalDocumentationObject struct { + Description string `json:"description,omitempty"` + URL string `json:"url,omitempty"` } // http://swagger.io/specification/#swaggerObject @@ -45,12 +70,14 @@ type swaggerPathItemObject struct { // http://swagger.io/specification/#operationObject type swaggerOperationObject struct { - Summary string `json:"summary"` + Summary string `json:"summary,omitempty"` Description string `json:"description,omitempty"` OperationId string `json:"operationId"` Responses swaggerResponsesObject `json:"responses"` Parameters swaggerParametersObject `json:"parameters,omitempty"` Tags []string `json:"tags,omitempty"` + + ExternalDocs *swaggerExternalDocumentationObject `json:"externalDocs,omitempty"` } type swaggerParametersObject []swaggerParameterObject From 497b8982e605b9b9fc690afdb40dbf3531d56fd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Vu=C4=8Dica?= Date: Sat, 7 May 2016 20:19:36 +0100 Subject: [PATCH 07/13] Swagger: Default response's key is now 200. While the spec does not seem to dictate this, most of the examples seem to have HTTP status codes as the keys. This patch replaces the text 'default' with value of '200'. --- examples/examplepb/echo_service.swagger.json | 4 ++-- .../streamless_everything.swagger.json | 18 +++++++++--------- protoc-gen-swagger/genswagger/template.go | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/examples/examplepb/echo_service.swagger.json b/examples/examplepb/echo_service.swagger.json index 39f226e1b09..dbc9ece0833 100644 --- a/examples/examplepb/echo_service.swagger.json +++ b/examples/examplepb/echo_service.swagger.json @@ -28,7 +28,7 @@ "description": "The message posted as the id parameter will also be\nreturned.", "operationId": "Echo", "responses": { - "default": { + "200": { "description": "Description", "schema": { "$ref": "#/definitions/examplepbSimpleMessage" @@ -58,7 +58,7 @@ "summary": "EchoBody method receives a simple message and returns it.", "operationId": "EchoBody", "responses": { - "default": { + "200": { "description": "Description", "schema": { "$ref": "#/definitions/examplepbSimpleMessage" diff --git a/examples/examplepb/streamless_everything.swagger.json b/examples/examplepb/streamless_everything.swagger.json index bd314024891..664d99557e9 100644 --- a/examples/examplepb/streamless_everything.swagger.json +++ b/examples/examplepb/streamless_everything.swagger.json @@ -19,7 +19,7 @@ "get": { "operationId": "List", "responses": { - "default": { + "200": { "description": "Description", "schema": { "$ref": "#/definitions/examplepbABitOfEverything" @@ -33,7 +33,7 @@ "post": { "operationId": "CreateBody", "responses": { - "default": { + "200": { "description": "Description", "schema": { "$ref": "#/definitions/examplepbABitOfEverything" @@ -59,7 +59,7 @@ "post": { "operationId": "BulkCreate", "responses": { - "default": { + "200": { "description": "Description", "schema": { "$ref": "#/definitions/examplepbEmptyMessage" @@ -85,7 +85,7 @@ "post": { "operationId": "BulkEcho", "responses": { - "default": { + "200": { "description": "Description", "schema": { "$ref": "#/definitions/subStringMessage" @@ -111,7 +111,7 @@ "get": { "operationId": "Echo", "responses": { - "default": { + "200": { "description": "Description", "schema": { "$ref": "#/definitions/subStringMessage" @@ -136,7 +136,7 @@ "post": { "operationId": "Create", "responses": { - "default": { + "200": { "description": "Description", "schema": { "$ref": "#/definitions/examplepbABitOfEverything" @@ -252,7 +252,7 @@ "get": { "operationId": "Lookup", "responses": { - "default": { + "200": { "description": "Description", "schema": { "$ref": "#/definitions/examplepbABitOfEverything" @@ -275,7 +275,7 @@ "delete": { "operationId": "Delete", "responses": { - "default": { + "200": { "description": "Description", "schema": { "$ref": "#/definitions/examplepbEmptyMessage" @@ -298,7 +298,7 @@ "put": { "operationId": "Update", "responses": { - "default": { + "200": { "description": "Description", "schema": { "$ref": "#/definitions/examplepbEmptyMessage" diff --git a/protoc-gen-swagger/genswagger/template.go b/protoc-gen-swagger/genswagger/template.go index 96239a9b550..0470199d256 100644 --- a/protoc-gen-swagger/genswagger/template.go +++ b/protoc-gen-swagger/genswagger/template.go @@ -456,7 +456,7 @@ func renderServices(services []*descriptor.Service, paths swaggerPathsObject, re OperationId: fmt.Sprintf("%s", meth.GetName()), Parameters: parameters, Responses: swaggerResponsesObject{ - "default": swaggerResponseObject{ + "200": swaggerResponseObject{ Description: "Description", Schema: swaggerSchemaObject{ Ref: fmt.Sprintf("#/definitions/%s", fullyQualifiedNameToSwaggerName(meth.ResponseType.FQMN(), reg)), From 0f189fd7d60775cd4bedb3182c636c55681707fa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Vu=C4=8Dica?= Date: Sat, 7 May 2016 22:51:40 +0100 Subject: [PATCH 08/13] Swagger: schema objects can have titles, too. --- protoc-gen-swagger/genswagger/types.go | 1 + 1 file changed, 1 insertion(+) diff --git a/protoc-gen-swagger/genswagger/types.go b/protoc-gen-swagger/genswagger/types.go index e2ed057f7a7..18ed850e669 100644 --- a/protoc-gen-swagger/genswagger/types.go +++ b/protoc-gen-swagger/genswagger/types.go @@ -129,6 +129,7 @@ type swaggerSchemaObject struct { Default string `json:"default,omitempty"` Description string `json:"description,omitempty"` + Title string `json:"title,omitempty"` } // http://swagger.io/specification/#referenceObject From 9a9e13516e02ad59e0af0132b7b02ba5d491934d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Vu=C4=8Dica?= Date: Sat, 7 May 2016 23:05:12 +0100 Subject: [PATCH 09/13] Title used a bit less often, which is important for swaggerSchemaObject. --- .../streamless_everything.swagger.json | 2 +- protoc-gen-swagger/genswagger/template.go | 27 ++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/examples/examplepb/streamless_everything.swagger.json b/examples/examplepb/streamless_everything.swagger.json index 664d99557e9..46c92c4bd73 100644 --- a/examples/examplepb/streamless_everything.swagger.json +++ b/examples/examplepb/streamless_everything.swagger.json @@ -426,7 +426,7 @@ "uint32_value": { "type": "integer", "format": "int64", - "description": "TODO(yugui) add bytes_value" + "title": "TODO(yugui) add bytes_value" }, "uint64_value": { "type": "integer", diff --git a/protoc-gen-swagger/genswagger/template.go b/protoc-gen-swagger/genswagger/template.go index 0470199d256..538376cd6de 100644 --- a/protoc-gen-swagger/genswagger/template.go +++ b/protoc-gen-swagger/genswagger/template.go @@ -558,13 +558,18 @@ func applyTemplate(p param) (string, error) { // will be updated instead. (JSON always gets applied directly to the passed // object.) // -// If there is no 'Summary', the same behavior will be attempted on 'Title'. +// If there is no 'Summary', the same behavior will be attempted on 'Title', +// but only if the last character is not a period. // // To apply additional Swagger properties, one can pass valid JSON as described // before. This JSON gets parsed and applied to the passed swaggerObject // directly. This lets users easily apply custom properties such as contact // details, API base path, et al. func updateSwaggerDataFromComments(swaggerObject interface{}, comment string) error { + if len(comment) == 0 { + return nil + } + // Find a section containing additional Swagger metadata. matches := swaggerExtrasRegexp.FindStringSubmatch(comment) @@ -597,8 +602,10 @@ func updateSwaggerDataFromComments(swaggerObject interface{}, comment string) er // Figure out which properties to update. summaryValue := infoObjectValue.FieldByName("Summary") descriptionValue := infoObjectValue.FieldByName("Description") + usingTitle := false if !summaryValue.CanSet() { summaryValue = infoObjectValue.FieldByName("Title") + usingTitle = true } // If there is a summary (or summary-equivalent), use the first @@ -608,16 +615,18 @@ func updateSwaggerDataFromComments(swaggerObject interface{}, comment string) er summary := strings.TrimSpace(paragraphs[0]) description := strings.TrimSpace(strings.Join(paragraphs[1:], "\n\n")) - if len(summary) > 0 { - summaryValue.Set(reflect.ValueOf(summary)) - } - if len(description) > 0 { - if !descriptionValue.CanSet() { - return fmt.Errorf("Object that has Summary but no Description?") + if !usingTitle || summary[len(summary)-1] != '.' { + if len(summary) > 0 { + summaryValue.Set(reflect.ValueOf(summary)) } - descriptionValue.Set(reflect.ValueOf(description)) + if len(description) > 0 { + if !descriptionValue.CanSet() { + return fmt.Errorf("Encountered object type with a summary, but no description") + } + descriptionValue.Set(reflect.ValueOf(description)) + } + return nil } - return nil } // There was no summary field on the swaggerObject. Try to apply the From 5f52c5dbec7ec170ec573cc1f98ec5ce446ab0f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ivan=20Vu=C4=8Dica?= Date: Sat, 7 May 2016 23:09:54 +0100 Subject: [PATCH 10/13] Backing out 'swagger extras' JSON code, per [request][1]. [1]: https://github.com/gengo/grpc-gateway/pull/134#issuecomment-217670498 --- examples/examplepb/echo_service.pb.go | 46 -------------------- examples/examplepb/echo_service.proto | 37 ---------------- examples/examplepb/echo_service.swagger.json | 14 +----- protoc-gen-swagger/genswagger/template.go | 39 +---------------- 4 files changed, 3 insertions(+), 133 deletions(-) diff --git a/examples/examplepb/echo_service.pb.go b/examples/examplepb/echo_service.pb.go index d191846eaec..fb676c649f4 100644 --- a/examples/examplepb/echo_service.pb.go +++ b/examples/examplepb/echo_service.pb.go @@ -10,25 +10,6 @@ Echo Service Echo Service API consists of a single service which returns a message. - - It is generated from these files: examples/examplepb/echo_service.proto examples/examplepb/a_bit_of_everything.proto @@ -66,15 +47,6 @@ var _ = math.Inf const _ = proto.ProtoPackageIsVersion1 // SimpleMessage represents a simple message sent to the Echo service. -// -// type SimpleMessage struct { // Id represents the message identifier. Id string `protobuf:"bytes,1,opt,name=id" json:"id,omitempty"` @@ -104,15 +76,6 @@ type EchoServiceClient interface { // // The message posted as the id parameter will also be // returned. - // - // Echo(ctx context.Context, in *SimpleMessage, opts ...grpc.CallOption) (*SimpleMessage, error) // EchoBody method receives a simple message and returns it. EchoBody(ctx context.Context, in *SimpleMessage, opts ...grpc.CallOption) (*SimpleMessage, error) @@ -151,15 +114,6 @@ type EchoServiceServer interface { // // The message posted as the id parameter will also be // returned. - // - // Echo(context.Context, *SimpleMessage) (*SimpleMessage, error) // EchoBody method receives a simple message and returns it. EchoBody(context.Context, *SimpleMessage) (*SimpleMessage, error) diff --git a/examples/examplepb/echo_service.proto b/examples/examplepb/echo_service.proto index 4baeddad06c..0573f29f052 100644 --- a/examples/examplepb/echo_service.proto +++ b/examples/examplepb/echo_service.proto @@ -5,39 +5,11 @@ option go_package = "examplepb"; // // Echo Service API consists of a single service which returns // a message. -// -// package gengo.grpc.gateway.examples.examplepb; import "google/api/annotations.proto"; // SimpleMessage represents a simple message sent to the Echo service. -// -// message SimpleMessage { // Id represents the message identifier. string id = 1; @@ -49,15 +21,6 @@ service EchoService { // // The message posted as the id parameter will also be // returned. - // - // rpc Echo(SimpleMessage) returns (SimpleMessage) { option (google.api.http) = { post: "/v1/example/echo/{id}" diff --git a/examples/examplepb/echo_service.swagger.json b/examples/examplepb/echo_service.swagger.json index dbc9ece0833..94e1abde9ac 100644 --- a/examples/examplepb/echo_service.swagger.json +++ b/examples/examplepb/echo_service.swagger.json @@ -3,14 +3,8 @@ "info": { "title": "Echo Service", "description": "Echo Service API consists of a single service which returns\na message.", - "version": "1.0", - "contact": { - "name": "gRPC-Gateway project", - "url": "https://github.com/gengo/grpc-gateway", - "email": "none@example.com" - } + "version": "version not set" }, - "host": "localhost", "schemes": [ "http", "https" @@ -46,11 +40,7 @@ ], "tags": [ "EchoService" - ], - "externalDocs": { - "description": "Find out more about EchoService", - "url": "http://github.com/gengo/grpc-gateway" - } + ] } }, "/v1/example/echo_body": { diff --git a/protoc-gen-swagger/genswagger/template.go b/protoc-gen-swagger/genswagger/template.go index 538376cd6de..88cb584715a 100644 --- a/protoc-gen-swagger/genswagger/template.go +++ b/protoc-gen-swagger/genswagger/template.go @@ -13,8 +13,6 @@ import ( pbdescriptor "github.com/golang/protobuf/protoc-gen-go/descriptor" ) -var swaggerExtrasRegexp = regexp.MustCompile(`(?s)^(.*[^\s])[\s]*[\s]*(.*)$`) - // findServicesMessagesAndEnumerations discovers all messages and enums defined in the RPC methods of the service. func findServicesMessagesAndEnumerations(s []*descriptor.Service, reg *descriptor.Registry, m messageMap, e enumMap) { for _, svc := range s { @@ -541,55 +539,20 @@ func applyTemplate(p param) (string, error) { // updateSwaggerDataFromComments updates a Swagger object based on a comment // from the proto file. // -// As a first step, a section matching: -// -// -// -// where .* contains valid JSON will be stored for later processing, and then -// removed from the passed string. -// (Implementation note: Currently, the JSON gets immediately applied and -// thus cannot override summary and description.) -// // First paragraph of a comment is used for summary. Remaining paragraphs of a // comment are used for description. If 'Summary' field is not present on the // passed swaggerObject, the summary and description are joined by \n\n. // // If there is a field named 'Info', its 'Summary' and 'Description' fields -// will be updated instead. (JSON always gets applied directly to the passed -// object.) +// will be updated instead. // // If there is no 'Summary', the same behavior will be attempted on 'Title', // but only if the last character is not a period. -// -// To apply additional Swagger properties, one can pass valid JSON as described -// before. This JSON gets parsed and applied to the passed swaggerObject -// directly. This lets users easily apply custom properties such as contact -// details, API base path, et al. func updateSwaggerDataFromComments(swaggerObject interface{}, comment string) error { if len(comment) == 0 { return nil } - // Find a section containing additional Swagger metadata. - matches := swaggerExtrasRegexp.FindStringSubmatch(comment) - - if len(matches) > 0 { - // If found, before further processing, replace the - // comment with a version that does not contain the - // extras. - comment = matches[1] - if len(matches[3]) > 0 { - comment += "\n\n" + matches[3] - } - - // Parse the JSON and apply it. - // TODO(ivucica): apply extras /after/ applying summary - // and description. - if err := json.Unmarshal([]byte(matches[2]), swaggerObject); err != nil { - return fmt.Errorf("error: %s, parsing: %s", err.Error(), matches[2]) - } - } - // Figure out what to apply changes to. swaggerObjectValue := reflect.ValueOf(swaggerObject) infoObjectValue := swaggerObjectValue.Elem().FieldByName("Info") From c423b4db440f8bb92f3260f9e3136bb728883b4a Mon Sep 17 00:00:00 2001 From: Yukinari Toyota Date: Fri, 13 May 2016 10:01:24 +0900 Subject: [PATCH 11/13] Fixes index out of range panic when empty. --- protoc-gen-swagger/genswagger/template.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/protoc-gen-swagger/genswagger/template.go b/protoc-gen-swagger/genswagger/template.go index 7f539984694..dcbd1a1381b 100644 --- a/protoc-gen-swagger/genswagger/template.go +++ b/protoc-gen-swagger/genswagger/template.go @@ -510,7 +510,7 @@ func updateSwaggerDataFromComments(swaggerObject interface{}, comment string) er summary := strings.TrimSpace(paragraphs[0]) description := strings.TrimSpace(strings.Join(paragraphs[1:], "\n\n")) - if !usingTitle || summary[len(summary)-1] != '.' { + if !usingTitle || summary == "" || summary[len(summary)-1] != '.' { if len(summary) > 0 { summaryValue.Set(reflect.ValueOf(summary)) } From 73c163ddf556f02eb6f49a78040b87b6a7de766a Mon Sep 17 00:00:00 2001 From: Yukinari Toyota Date: Fri, 13 May 2016 20:35:27 +0900 Subject: [PATCH 12/13] Fixes missing field descriptions if the Message has index 2 --- examples/examplepb/streamless_everything.proto | 1 + examples/examplepb/streamless_everything.swagger.json | 3 ++- protoc-gen-swagger/genswagger/template.go | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/examplepb/streamless_everything.proto b/examples/examplepb/streamless_everything.proto index 4e9f20387a9..8284826646f 100644 --- a/examples/examplepb/streamless_everything.proto +++ b/examples/examplepb/streamless_everything.proto @@ -59,6 +59,7 @@ message EmptyMessage { } message IdMessage { + // uuid is UUID value string uuid = 1; } diff --git a/examples/examplepb/streamless_everything.swagger.json b/examples/examplepb/streamless_everything.swagger.json index a9eca281655..d16aa6b16d4 100644 --- a/examples/examplepb/streamless_everything.swagger.json +++ b/examples/examplepb/streamless_everything.swagger.json @@ -520,7 +520,8 @@ "properties": { "uuid": { "type": "string", - "format": "string" + "format": "string", + "title": "uuid is UUID value" } } }, diff --git a/protoc-gen-swagger/genswagger/template.go b/protoc-gen-swagger/genswagger/template.go index dcbd1a1381b..caf12df68d6 100644 --- a/protoc-gen-swagger/genswagger/template.go +++ b/protoc-gen-swagger/genswagger/template.go @@ -564,7 +564,7 @@ func protoComments(reg *descriptor.Registry, file *descriptor.File, outers []str packageProtoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "Package") L1: for _, loc := range file.SourceCodeInfo.Location { - if typeIndex != packageProtoPath { + if typeName != "Package" || typeIndex != packageProtoPath { if len(loc.Path) < len(outerPaths)*2+2+len(fieldPaths) { continue } From c4d2c59f34688716410c56437368172a24e3962f Mon Sep 17 00:00:00 2001 From: Yukinari Toyota Date: Fri, 13 May 2016 21:19:21 +0900 Subject: [PATCH 13/13] Refactor proto comment extractor --- protoc-gen-swagger/genswagger/template.go | 101 ++++++++++++---------- 1 file changed, 54 insertions(+), 47 deletions(-) diff --git a/protoc-gen-swagger/genswagger/template.go b/protoc-gen-swagger/genswagger/template.go index caf12df68d6..93bf01ebc68 100644 --- a/protoc-gen-swagger/genswagger/template.go +++ b/protoc-gen-swagger/genswagger/template.go @@ -559,54 +559,9 @@ func protoComments(reg *descriptor.Registry, file *descriptor.File, outers []str outerPaths[i] = int32(msg.Index) } - messageProtoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "MessageType") - nestedProtoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), "NestedType") - packageProtoPath := protoPathIndex(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "Package") -L1: for _, loc := range file.SourceCodeInfo.Location { - if typeName != "Package" || typeIndex != packageProtoPath { - if len(loc.Path) < len(outerPaths)*2+2+len(fieldPaths) { - continue - } - for i, v := range outerPaths { - if i == 0 && loc.Path[i*2+0] != messageProtoPath { - continue L1 - } - if i != 0 && loc.Path[i*2+0] != nestedProtoPath { - continue L1 - } - if loc.Path[i*2+1] != v { - continue L1 - } - } - - outerOffset := len(outerPaths) * 2 - if outerOffset == 0 && loc.Path[outerOffset] != protoPathIndex(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), typeName) { - continue - } - if outerOffset != 0 { - if typeName == "MessageType" { - typeName = "NestedType" - } - if loc.Path[outerOffset] != protoPathIndex(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), typeName) { - continue - } - } - if loc.Path[outerOffset+1] != typeIndex { - continue - } - - for i, v := range fieldPaths { - if loc.Path[outerOffset+2+i] != v { - continue L1 - } - } - } else { - // path for package comments is just [2], and all the other processing - // is too complex for it. - if len(loc.Path) == 0 || typeIndex != loc.Path[0] { - continue - } + if !isProtoPathMatches(loc.Path, outerPaths, typeName, typeIndex, fieldPaths) { + continue } comments := "" if loc.LeadingComments != nil { @@ -625,6 +580,58 @@ L1: return "" } +var messageProtoPath = protoPathIndex(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "MessageType") +var nestedProtoPath = protoPathIndex(reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)), "NestedType") +var packageProtoPath = protoPathIndex(reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)), "Package") + +func isProtoPathMatches(paths []int32, outerPaths []int32, typeName string, typeIndex int32, fieldPaths []int32) bool { + if typeName == "Package" && typeIndex == packageProtoPath { + // path for package comments is just [2], and all the other processing + // is too complex for it. + if len(paths) == 0 || typeIndex != paths[0] { + return false + } + return true + } + + if len(paths) != len(outerPaths)*2+2+len(fieldPaths) { + return false + } + + typeNameDescriptor := reflect.TypeOf((*pbdescriptor.FileDescriptorProto)(nil)) + if len(outerPaths) > 0 { + if paths[0] != messageProtoPath || paths[1] != outerPaths[0] { + return false + } + paths = paths[2:] + outerPaths = outerPaths[1:] + + for i, v := range outerPaths { + if paths[i*2] != nestedProtoPath || paths[i*2+1] != v { + return false + } + } + paths = paths[len(outerPaths)*2:] + + if typeName == "MessageType" { + typeName = "NestedType" + } + typeNameDescriptor = reflect.TypeOf((*pbdescriptor.DescriptorProto)(nil)) + } + + if paths[0] != protoPathIndex(typeNameDescriptor, typeName) || paths[1] != typeIndex { + return false + } + paths = paths[2:] + + for i, v := range fieldPaths { + if paths[i] != v { + return false + } + } + return true +} + // protoPathIndex returns a path component for google.protobuf.descriptor.SourceCode_Location. // // Specifically, it returns an id as generated from descriptor proto which