Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions protoc-gen-openapiv2/internal/genopenapi/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -1294,11 +1294,33 @@ func renderServiceTags(services []*descriptor.Service, reg *descriptor.Registry)
tag.Name = opts.GetName()
}
}

// If no description is set from options, use proto comments
if tag.Description == "" {
svcIdx := findServiceIndex(svc)
if svcIdx >= 0 {
svcComments := protoComments(reg, svc.File, nil, "Service", int32(svcIdx))
if err := updateOpenAPIDataFromComments(reg, &tag, svc, svcComments, false); err != nil {
grpclog.Error(err)
}
}
}

tags = append(tags, tag)
}
return tags
}

// findServiceIndex finds the index of a service within its file's service list.
func findServiceIndex(svc *descriptor.Service) int {
for i, s := range svc.File.Services {
if s == svc {
return i
}
}
return -1
}

// expandPathPatterns searches the URI parts for path parameters with pattern and when the pattern contains a sub-path,
// it expands the pattern into the URI parts and adds the new path parameters to the pathParams slice.
//
Expand Down
192 changes: 192 additions & 0 deletions protoc-gen-openapiv2/internal/genopenapi/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12025,6 +12025,7 @@ func Test_updateSwaggerObjectFromFieldBehavior(t *testing.T) {
})
}
}

// TestBodyParameterRequiredFieldBug tests the bug where the body parameter name
// is incorrectly added to the schema's required array when using body: "field_name"
func TestBodyParameterRequiredFieldBug(t *testing.T) {
Expand Down Expand Up @@ -12724,3 +12725,194 @@ func TestBodyParameterSelfReferentialBug(t *testing.T) {
bodyParam.Schema.Required, expectedRequired, cmp.Diff(expectedRequired, bodyParam.Schema.Required))
}
}

func TestRenderServiceTagsWithProtoComments(t *testing.T) {
svc := &descriptorpb.ServiceDescriptorProto{
Name: proto.String("ExampleService"),
}

serviceComment := "This is a service-level comment.\n\nIt has multiple paragraphs."
// The service path in proto source code info is [6, service_index]
// where 6 is the field number for 'service' in FileDescriptorProto
servicePath := []int32{6, 0}

file := &descriptor.File{
FileDescriptorProto: &descriptorpb.FileDescriptorProto{
SourceCodeInfo: &descriptorpb.SourceCodeInfo{
Location: []*descriptorpb.SourceCodeInfo_Location{
{
Path: servicePath,
LeadingComments: &serviceComment,
},
},
},
Name: proto.String("example.proto"),
Package: proto.String("example"),
Service: []*descriptorpb.ServiceDescriptorProto{svc},
},
}

services := []*descriptor.Service{
{
ServiceDescriptorProto: svc,
File: file,
},
}
file.Services = services

reg := descriptor.NewRegistry()

tags := renderServiceTags(services, reg)

if len(tags) != 1 {
t.Fatalf("expected 1 tag, got %d", len(tags))
}

// The description should be populated from the proto comment
// Since openapiTagObject doesn't have a Summary field, the entire comment
// goes into the Description field
expectedDesc := "This is a service-level comment.\n\nIt has multiple paragraphs."
if tags[0].Description != expectedDesc {
t.Errorf("renderServiceTags().Tags[0].Description = %q, want %q", tags[0].Description, expectedDesc)
}

if tags[0].Name != "ExampleService" {
t.Errorf("renderServiceTags().Tags[0].Name = %q, want %q", tags[0].Name, "ExampleService")
}
}

func TestRenderServiceTagsWithSingleParagraphComment(t *testing.T) {
svc := &descriptorpb.ServiceDescriptorProto{
Name: proto.String("ExampleService"),
}

serviceComment := "This is a single paragraph service comment."
servicePath := []int32{6, 0}

file := &descriptor.File{
FileDescriptorProto: &descriptorpb.FileDescriptorProto{
SourceCodeInfo: &descriptorpb.SourceCodeInfo{
Location: []*descriptorpb.SourceCodeInfo_Location{
{
Path: servicePath,
LeadingComments: &serviceComment,
},
},
},
Name: proto.String("example.proto"),
Package: proto.String("example"),
Service: []*descriptorpb.ServiceDescriptorProto{svc},
},
}

services := []*descriptor.Service{
{
ServiceDescriptorProto: svc,
File: file,
},
}
file.Services = services

reg := descriptor.NewRegistry()

tags := renderServiceTags(services, reg)

if len(tags) != 1 {
t.Fatalf("expected 1 tag, got %d", len(tags))
}

// For a single paragraph without explicit title, the whole comment becomes the description
expectedDesc := "This is a single paragraph service comment."
if tags[0].Description != expectedDesc {
t.Errorf("renderServiceTags().Tags[0].Description = %q, want %q", tags[0].Description, expectedDesc)
}
}

func TestRenderServiceTagsExplicitOptionTakesPrecedence(t *testing.T) {
svc := &descriptorpb.ServiceDescriptorProto{
Name: proto.String("ExampleService"),
Options: &descriptorpb.ServiceOptions{},
}

// Set explicit OpenAPI option on the service descriptor using proto extension
proto.SetExtension(svc.Options, openapi_options.E_Openapiv2Tag, &openapi_options.Tag{
Description: "Explicit option description",
})

serviceComment := "This is a service-level comment from proto."
servicePath := []int32{6, 0}

file := &descriptor.File{
FileDescriptorProto: &descriptorpb.FileDescriptorProto{
SourceCodeInfo: &descriptorpb.SourceCodeInfo{
Location: []*descriptorpb.SourceCodeInfo_Location{
{
Path: servicePath,
LeadingComments: &serviceComment,
},
},
},
Name: proto.String("example.proto"),
Package: proto.String("example"),
Service: []*descriptorpb.ServiceDescriptorProto{svc},
},
}

services := []*descriptor.Service{
{
ServiceDescriptorProto: svc,
File: file,
},
}
file.Services = services

reg := descriptor.NewRegistry()

tags := renderServiceTags(services, reg)

if len(tags) != 1 {
t.Fatalf("expected 1 tag, got %d", len(tags))
}

// The explicit option should take precedence over proto comment
expectedDesc := "Explicit option description"
if tags[0].Description != expectedDesc {
t.Errorf("renderServiceTags().Tags[0].Description = %q, want %q", tags[0].Description, expectedDesc)
}
}

func TestRenderServiceTagsNoComment(t *testing.T) {
svc := &descriptorpb.ServiceDescriptorProto{
Name: proto.String("ExampleService"),
}

file := &descriptor.File{
FileDescriptorProto: &descriptorpb.FileDescriptorProto{
SourceCodeInfo: &descriptorpb.SourceCodeInfo{},
Name: proto.String("example.proto"),
Package: proto.String("example"),
Service: []*descriptorpb.ServiceDescriptorProto{svc},
},
}

services := []*descriptor.Service{
{
ServiceDescriptorProto: svc,
File: file,
},
}
file.Services = services

reg := descriptor.NewRegistry()

tags := renderServiceTags(services, reg)

if len(tags) != 1 {
t.Fatalf("expected 1 tag, got %d", len(tags))
}

// No comment should result in empty description
if tags[0].Description != "" {
t.Errorf("renderServiceTags().Tags[0].Description = %q, want empty string", tags[0].Description)
}
}
Loading