Skip to content

Commit ab13a7a

Browse files
authored
feat(internal/sidekick): use []PathSegment type for Resource.Pattern (#3220)
This PR updates the `Resource.Pattern` field in `internal/sidekick/api/model.go` from `Pattern []string` to `Patterns []ResourcePattern`. This change allows for a more structured representation of resource name patterns. The following changes are included: - The `Resource.Pattern` field is now `Resource.Patterns`. - The `Resource.Patterns` field is now of type `[]ResourcePattern`. - `internal/sidekick/parser/protobuf.go` is updated to parse the pattern strings into `[]PathSegment` objects. - A new `ParseResourcePattern` function was added to `internal/sidekick/parser/httprule/http_rule_parser.go` to handle resource patterns that do not have a leading `/`. - Improved error handling within `internal/sidekick/parser/protobuf.go` to provide better context and diagnostics. - `processMethod`, `processResourceReference`, `processResourceAnnotation`, and `processResourceDefinitions` now return detailed errors instead of silently failing. - Error messages have been enriched with contextual information such as message IDs, field IDs, and filenames to help diagnose any errors. Fixes #3090
1 parent 9f691cd commit ab13a7a

File tree

6 files changed

+284
-85
lines changed

6 files changed

+284
-85
lines changed

internal/sidekick/api/model.go

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -597,6 +597,23 @@ func (v *PathVariable) WithMatch() *PathVariable {
597597
return v
598598
}
599599

600+
// NewPathSegment creates a new path segment.
601+
func NewPathSegment() *PathSegment {
602+
return &PathSegment{}
603+
}
604+
605+
// WithLiteral adds a literal to the path segment.
606+
func (s *PathSegment) WithLiteral(l string) *PathSegment {
607+
s.Literal = &l
608+
return s
609+
}
610+
611+
// WithVariable adds a variable to the path segment.
612+
func (s *PathSegment) WithVariable(v *PathVariable) *PathSegment {
613+
s.Variable = v
614+
return s
615+
}
616+
600617
// Message defines a message used in request/response handling.
601618
type Message struct {
602619
// Documentation for the message.
@@ -919,13 +936,9 @@ type Resource struct {
919936
// Type identifies the kind of resource (e.g., "cloudresourcemanager.googleapis.com/Project").
920937
// This string is globally unique and identifies the type of resource across Google Cloud.
921938
Type string
922-
// Pattern is a list of strings representing the resource name pattern,
923-
// defining the structure of its unique identifier. For example, a pattern
924-
// might be `["publishers", "{publisher}", "shelves", "{shelf}"]`.
925-
// These patterns are used to construct and parse resource names.
926-
// TODO(https://github.com/googleapis/librarian/issues/3090): Pattern field in Resource struct
927-
// should be []PathSegment
928-
Pattern []string
939+
// Pattern is a list of resource patterns, where each pattern is a sequence of path segments.
940+
// This defines the structure of the resource's unique identifier.
941+
Patterns []ResourcePattern
929942
// Plural is the plural form of the resource name.
930943
// For example, for a "Book" resource, Plural would be "books".
931944
Plural string
@@ -940,6 +953,9 @@ type Resource struct {
940953
Codec any
941954
}
942955

956+
// ResourcePattern is a sequence of path segments that defines the structure of a resource's unique identifier.
957+
type ResourcePattern []PathSegment
958+
943959
// ResourceReference describes a field's relationship to another resource type.
944960
// It acts as a foreign key, indicating that the field's value identifies an instance of another resource.
945961
// This relationship is established via the `google.api.resource_reference` annotation in Protobuf.

internal/sidekick/parser/httprule/http_rule_parser.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,13 @@ func ParseSegments(pathTemplate string) (*api.PathTemplate, error) {
8787
return parsePathTemplate(pathTemplate)
8888
}
8989

90+
// ParseResourcePattern parses a Resource Pattern, defined at the [google.api.resource annotation].
91+
//
92+
// This is different from `ParseSegments` because it does not expect a leading `/`.
93+
func ParseResourcePattern(pathTemplate string) (*api.PathTemplate, error) {
94+
return parsePathTemplate("/" + pathTemplate)
95+
}
96+
9097
// Literal is a literal in a path template.
9198
type Literal string
9299

internal/sidekick/parser/protobuf.go

Lines changed: 73 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"github.com/googleapis/librarian/internal/serviceconfig"
2929
"github.com/googleapis/librarian/internal/sidekick/api"
3030
"github.com/googleapis/librarian/internal/sidekick/config"
31+
"github.com/googleapis/librarian/internal/sidekick/parser/httprule"
3132
"github.com/googleapis/librarian/internal/sidekick/parser/svcconfig"
3233
"github.com/googleapis/librarian/internal/sidekick/protobuf"
3334
"google.golang.org/genproto/googleapis/api/annotations"
@@ -48,7 +49,7 @@ func ParseProtobuf(cfg *config.Config) (*api.API, error) {
4849
if err != nil {
4950
return nil, err
5051
}
51-
return makeAPIForProtobuf(serviceConfig, request), nil
52+
return makeAPIForProtobuf(serviceConfig, request)
5253
}
5354

5455
func newCodeGeneratorRequest(source string, options map[string]string) (_ *pluginpb.CodeGeneratorRequest, err error) {
@@ -180,7 +181,7 @@ const (
180181
enumDescriptorValue = 2
181182
)
182183

183-
func makeAPIForProtobuf(serviceConfig *serviceconfig.Service, req *pluginpb.CodeGeneratorRequest) *api.API {
184+
func makeAPIForProtobuf(serviceConfig *serviceconfig.Service, req *pluginpb.CodeGeneratorRequest) (*api.API, error) {
184185
var (
185186
mixinFileDesc []*descriptorpb.FileDescriptorProto
186187
enabledMixinMethods mixinMethods = make(map[string]bool)
@@ -215,14 +216,18 @@ func makeAPIForProtobuf(serviceConfig *serviceconfig.Service, req *pluginpb.Code
215216
fFQN := "." + f.GetPackage()
216217
for _, m := range f.MessageType {
217218
mFQN := fFQN + "." + m.GetName()
218-
_ = processMessage(state, m, mFQN, f.GetPackage(), nil)
219+
if _, err := processMessage(state, m, mFQN, f.GetPackage(), nil); err != nil {
220+
return nil, err
221+
}
219222
}
220223

221224
for _, e := range f.EnumType {
222225
eFQN := fFQN + "." + e.GetName()
223226
_ = processEnum(state, e, eFQN, f.GetPackage(), nil)
224227
}
225-
processResourceDefinitions(f, result)
228+
if err := processResourceDefinitions(f, result); err != nil {
229+
return nil, err
230+
}
226231
}
227232

228233
// Then we need to add the messages, enums and services to the list of
@@ -257,9 +262,11 @@ func makeAPIForProtobuf(serviceConfig *serviceconfig.Service, req *pluginpb.Code
257262
service := processService(state, s, sFQN, f.GetPackage())
258263
for _, m := range s.Method {
259264
mFQN := sFQN + "." + m.GetName()
260-
if method := processMethod(state, m, mFQN, f.GetPackage(), sFQN); method != nil {
261-
service.Methods = append(service.Methods, method)
265+
method, err := processMethod(state, m, mFQN, f.GetPackage(), sFQN)
266+
if err != nil {
267+
return nil, err
262268
}
269+
service.Methods = append(service.Methods, method)
263270
}
264271
fileServices = append(fileServices, service)
265272
}
@@ -317,10 +324,12 @@ func makeAPIForProtobuf(serviceConfig *serviceconfig.Service, req *pluginpb.Code
317324
// define the mixin method in the code.
318325
continue
319326
}
320-
if method := processMethod(state, m, mFQN, service.Package, sFQN); method != nil {
321-
applyServiceConfigMethodOverrides(method, originalFQN, serviceConfig, result, mixin)
322-
service.Methods = append(service.Methods, method)
327+
method, err := processMethod(state, m, mFQN, service.Package, sFQN)
328+
if err != nil {
329+
return nil, err
323330
}
331+
applyServiceConfigMethodOverrides(method, originalFQN, serviceConfig, result, mixin)
332+
service.Methods = append(service.Methods, method)
324333
}
325334
}
326335
}
@@ -331,7 +340,7 @@ func makeAPIForProtobuf(serviceConfig *serviceconfig.Service, req *pluginpb.Code
331340
}
332341
updatePackageName(result)
333342
updateAutoPopulatedFields(serviceConfig, result)
334-
return result
343+
return result, nil
335344
}
336345

337346
// requiresLongrunningMixin finds out if any method returns a LRO. This is used
@@ -437,16 +446,14 @@ func processService(state *api.APIState, s *descriptorpb.ServiceDescriptorProto,
437446
return service
438447
}
439448

440-
func processMethod(state *api.APIState, m *descriptorpb.MethodDescriptorProto, mFQN, packagez, serviceID string) *api.Method {
449+
func processMethod(state *api.APIState, m *descriptorpb.MethodDescriptorProto, mFQN, packagez, serviceID string) (*api.Method, error) {
441450
pathInfo, err := parsePathInfo(m, state)
442451
if err != nil {
443-
slog.Error("unsupported http method", "method", m, "error", err)
444-
return nil
452+
return nil, fmt.Errorf("unsupported http method for %q: %w", mFQN, err)
445453
}
446454
routing, err := parseRoutingAnnotations(mFQN, m)
447455
if err != nil {
448-
slog.Error("cannot parse routing annotations", "method", m, "err", err)
449-
return nil
456+
return nil, fmt.Errorf("cannot parse routing annotations for %q: %w", mFQN, err)
450457
}
451458
outputTypeID := m.GetOutputType()
452459
method := &api.Method{
@@ -464,10 +471,10 @@ func processMethod(state *api.APIState, m *descriptorpb.MethodDescriptorProto, m
464471
SourceServiceID: serviceID,
465472
}
466473
state.MethodByID[mFQN] = method
467-
return method
474+
return method, nil
468475
}
469476

470-
func processMessage(state *api.APIState, m *descriptorpb.DescriptorProto, mFQN, packagez string, parent *api.Message) *api.Message {
477+
func processMessage(state *api.APIState, m *descriptorpb.DescriptorProto, mFQN, packagez string, parent *api.Message) (*api.Message, error) {
471478
message := &api.Message{
472479
Name: m.GetName(),
473480
ID: mFQN,
@@ -481,12 +488,17 @@ func processMessage(state *api.APIState, m *descriptorpb.DescriptorProto, mFQN,
481488
if opts.GetMapEntry() {
482489
message.IsMap = true
483490
}
484-
processResourceAnnotation(opts, message)
491+
if err := processResourceAnnotation(opts, message); err != nil {
492+
return nil, err
493+
}
485494
}
486495
if len(m.GetNestedType()) > 0 {
487496
for _, nm := range m.GetNestedType() {
488497
nmFQN := mFQN + "." + nm.GetName()
489-
nmsg := processMessage(state, nm, nmFQN, packagez, message)
498+
nmsg, err := processMessage(state, nm, nmFQN, packagez, message)
499+
if err != nil {
500+
return nil, err
501+
}
490502
message.Messages = append(message.Messages, nmsg)
491503
}
492504
}
@@ -514,7 +526,9 @@ func processMessage(state *api.APIState, m *descriptorpb.DescriptorProto, mFQN,
514526
AutoPopulated: protobufIsAutoPopulated(mf),
515527
Behavior: protobufFieldBehavior(mf),
516528
}
517-
processResourceReference(mf, field)
529+
if err := processResourceReference(mf, field); err != nil {
530+
return nil, err
531+
}
518532
normalizeTypes(state, mf, field)
519533
message.Fields = append(message.Fields, field)
520534
if field.IsOneOf {
@@ -536,46 +550,59 @@ func processMessage(state *api.APIState, m *descriptorpb.DescriptorProto, mFQN,
536550
message.OneOfs = message.OneOfs[:oneOfIdx]
537551
}
538552

539-
return message
553+
return message, nil
540554
}
541555

542-
func processResourceAnnotation(opts *descriptorpb.MessageOptions, message *api.Message) {
556+
func processResourceAnnotation(opts *descriptorpb.MessageOptions, message *api.Message) error {
543557
if !proto.HasExtension(opts, annotations.E_Resource) {
544-
return
558+
return nil
545559
}
546560
ext := proto.GetExtension(opts, annotations.E_Resource)
547561
res, ok := ext.(*annotations.ResourceDescriptor)
548562
if !ok {
549-
return
563+
return fmt.Errorf("in message %q: unexpected type for E_Resource extension: %T", message.ID, ext)
564+
}
565+
566+
patterns, err := parseResourcePatterns(res.GetPattern())
567+
if err != nil {
568+
return fmt.Errorf("in message %q: %w", message.ID, err)
550569
}
570+
551571
message.Resource = &api.Resource{
552572
Type: res.GetType(),
553-
Pattern: res.GetPattern(),
573+
Patterns: patterns,
554574
Plural: res.GetPlural(),
555575
Singular: res.GetSingular(),
556576
Self: message,
557577
}
578+
return nil
558579
}
559580

560-
func processResourceDefinitions(f *descriptorpb.FileDescriptorProto, result *api.API) {
581+
func processResourceDefinitions(f *descriptorpb.FileDescriptorProto, result *api.API) error {
561582
if f.Options == nil || !proto.HasExtension(f.Options, annotations.E_ResourceDefinition) {
562-
return
583+
return nil
563584
}
564585

565586
ext := proto.GetExtension(f.Options, annotations.E_ResourceDefinition)
566587
res, ok := ext.([]*annotations.ResourceDescriptor)
567588
if !ok {
568-
return
589+
return fmt.Errorf("unexpected type for E_ResourceDefinition extension: %T", ext)
569590
}
570591

571592
for _, r := range res {
593+
patterns, err := parseResourcePatterns(r.GetPattern())
594+
if err != nil {
595+
return fmt.Errorf("in file %q: %w", f.GetName(), err)
596+
}
597+
572598
result.ResourceDefinitions = append(result.ResourceDefinitions, &api.Resource{
573599
Type: r.GetType(),
574-
Pattern: r.GetPattern(),
600+
Patterns: patterns,
575601
Plural: r.GetPlural(),
576602
Singular: r.GetSingular(),
577603
})
578604
}
605+
return nil
579606
}
580607

581608
// TODO(https://github.com/googleapis/librarian/issues/3036): This function needs
@@ -591,22 +618,23 @@ func processResourceDefinitions(f *descriptorpb.FileDescriptorProto, result *api
591618
// distinction correctly. Future work should involve creating a more robust
592619
// model that correctly determines the primary resource for a method, using
593620
// `child_type` when it is present for collection-based methods.
594-
func processResourceReference(f *descriptorpb.FieldDescriptorProto, field *api.Field) {
621+
func processResourceReference(f *descriptorpb.FieldDescriptorProto, field *api.Field) error {
595622
if f.Options == nil {
596-
return
623+
return nil
597624
}
598625
if !proto.HasExtension(f.Options, annotations.E_ResourceReference) {
599-
return
626+
return nil
600627
}
601628
ext := proto.GetExtension(f.Options, annotations.E_ResourceReference)
602629
ref, ok := ext.(*annotations.ResourceReference)
603630
if !ok {
604-
return
631+
return fmt.Errorf("in field %q: unexpected type for E_ResourceReference extension: %T", field.ID, ext)
605632
}
606633
field.ResourceReference = &api.ResourceReference{
607634
Type: ref.Type,
608635
ChildType: ref.ChildType,
609636
}
637+
return nil
610638
}
611639

612640
func processEnum(state *api.APIState, e *descriptorpb.EnumDescriptorProto, eFQN, packagez string, parent *api.Message) *api.Enum {
@@ -708,6 +736,18 @@ func addEnumDocumentation(state *api.APIState, p []int32, doc string, eFQN strin
708736
}
709737
}
710738

739+
func parseResourcePatterns(patterns []string) ([]api.ResourcePattern, error) {
740+
var parsedPatterns []api.ResourcePattern
741+
for _, p := range patterns {
742+
tmpl, err := httprule.ParseResourcePattern(p)
743+
if err != nil {
744+
return nil, fmt.Errorf("failed to parse resource pattern %q: %w", p, err)
745+
}
746+
parsedPatterns = append(parsedPatterns, tmpl.Segments)
747+
}
748+
return parsedPatterns, nil
749+
}
750+
711751
// trimLeadingSpacesInDocumentation removes the leading spaces from each line in the documentation.
712752
// Protobuf removes the `//` leading characters, but leaves the leading
713753
// whitespace. It is easier to reason about the comments in the rest of the

internal/sidekick/parser/protobuf_mixin_test.go

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,10 @@ func TestProtobuf_LocationMixin(t *testing.T) {
5252
},
5353
},
5454
}
55-
test := makeAPIForProtobuf(serviceConfig, newTestCodeGeneratorRequest(t, "test_service.proto"))
55+
test, err := makeAPIForProtobuf(serviceConfig, newTestCodeGeneratorRequest(t, "test_service.proto"))
56+
if err != nil {
57+
t.Fatalf("Failed to make API for Protobuf: %v", err)
58+
}
5659
for _, service := range test.Services {
5760
if service.ID == ".google.cloud.location.Locations" {
5861
t.Fatalf("Mixin %s should not be in list of services to generate", service.ID)
@@ -120,7 +123,10 @@ func TestProtobuf_IAMMixin(t *testing.T) {
120123
},
121124
},
122125
}
123-
test := makeAPIForProtobuf(serviceConfig, newTestCodeGeneratorRequest(t, "test_service.proto"))
126+
test, err := makeAPIForProtobuf(serviceConfig, newTestCodeGeneratorRequest(t, "test_service.proto"))
127+
if err != nil {
128+
t.Fatalf("Failed to make API for Protobuf: %v", err)
129+
}
124130
for _, service := range test.Services {
125131
if service.ID == ".google.iam.v1.IAMPolicy" {
126132
t.Fatalf("Mixin %s should not be in list of services to generate", service.ID)
@@ -194,7 +200,10 @@ func TestProtobuf_OperationMixin(t *testing.T) {
194200
},
195201
},
196202
}
197-
test := makeAPIForProtobuf(serviceConfig, newTestCodeGeneratorRequest(t, "test_service.proto"))
203+
test, err := makeAPIForProtobuf(serviceConfig, newTestCodeGeneratorRequest(t, "test_service.proto"))
204+
if err != nil {
205+
t.Fatalf("Failed to make API for Protobuf: %v", err)
206+
}
198207
for _, service := range test.Services {
199208
if service.ID == ".google.longrunning.Operations" {
200209
t.Fatalf("Mixin %s should not be in list of services to generate", service.ID)
@@ -278,7 +287,10 @@ func TestProtobuf_OperationMixinNoEmpty(t *testing.T) {
278287
},
279288
},
280289
}
281-
test := makeAPIForProtobuf(serviceConfig, newTestCodeGeneratorRequest(t, "test_noempty_mixin.proto"))
290+
test, err := makeAPIForProtobuf(serviceConfig, newTestCodeGeneratorRequest(t, "test_noempty_mixin.proto"))
291+
if err != nil {
292+
t.Fatalf("Failed to make API for Protobuf: %v", err)
293+
}
282294
for _, service := range test.Services {
283295
if service.ID == ".google.longrunning.Operations" {
284296
t.Fatalf("Mixin %s should not be in list of services to generate", service.ID)
@@ -361,7 +373,10 @@ func TestProtobuf_DuplicateMixin(t *testing.T) {
361373
},
362374
},
363375
}
364-
test := makeAPIForProtobuf(serviceConfig, newTestCodeGeneratorRequest(t, "test_duplicate_mixin.proto"))
376+
test, err := makeAPIForProtobuf(serviceConfig, newTestCodeGeneratorRequest(t, "test_duplicate_mixin.proto"))
377+
if err != nil {
378+
t.Fatalf("Failed to make API for Protobuf: %v", err)
379+
}
365380
for _, service := range test.Services {
366381
if service.ID == ".google.longrunning.Operations" {
367382
t.Fatalf("Mixin %s should not be in list of services to generate", service.ID)

0 commit comments

Comments
 (0)