Skip to content

Commit 1d4dfd3

Browse files
feat(sidekick/rust): Generate samples for non-LRO delete RPCs (#3319)
1 parent f05b7bb commit 1d4dfd3

File tree

6 files changed

+201
-8
lines changed

6 files changed

+201
-8
lines changed

internal/sidekick/api/model.go

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ type APIState struct {
188188
MessageByID map[string]*Message
189189
// EnumByID returns a message that is associated with the API.
190190
EnumByID map[string]*Enum
191+
// ResourceByType returns a resource that is associated with the API.
192+
ResourceByType map[string]*Resource
191193
}
192194

193195
// Service represents a service in an API.
@@ -373,7 +375,8 @@ func (m *Method) IsSimple() bool {
373375
// IsAIPStandard returns true if the method is one of the AIP standard methods.
374376
// IsAIPStandard simplifies writing mustache templates, mostly for samples.
375377
func (m *Method) IsAIPStandard() bool {
376-
return m.AIPStandardGetInfo() != nil
378+
return m.AIPStandardGetInfo() != nil ||
379+
m.AIPStandardDeleteInfo() != nil
377380
}
378381

379382
// AIPStandardGetInfo contains information relevant to get operations as defined by AIP-131.
@@ -413,6 +416,53 @@ func (m *Method) AIPStandardGetInfo() *AIPStandardGetInfo {
413416
}
414417
}
415418

419+
// AIPStandardDeleteInfo contains information relevant to delete operations as defined by AIP-135.
420+
type AIPStandardDeleteInfo struct {
421+
// ResourceNameRequestField is the field in the method input that contains the resource name
422+
// of the resource that the delete operation should delete.
423+
ResourceNameRequestField *Field
424+
}
425+
426+
// AIPStandardDeleteInfo returns information relevant to a delete operation as defined by AIP-135
427+
// if the method is such an operation.
428+
func (m *Method) AIPStandardDeleteInfo() *AIPStandardDeleteInfo {
429+
// A delete operation is either simple or LRO.
430+
if !m.IsSimple() && m.OperationInfo == nil {
431+
return nil
432+
}
433+
434+
// Standard delete methods for resource "Foo" should be named "DeleteFoo".
435+
maybeSingular, found := strings.CutPrefix(strings.ToLower(m.Name), "delete")
436+
if !found || maybeSingular == "" {
437+
return nil
438+
}
439+
// The request name should be "DeleteFooRequest".
440+
if m.InputType == nil ||
441+
strings.ToLower(m.InputType.Name) != fmt.Sprintf("delete%srequest", maybeSingular) {
442+
return nil
443+
}
444+
445+
// Find the field in the request that is a resource reference of a resource
446+
// whose singular name is maybeSingular.
447+
fieldIndex := slices.IndexFunc(m.InputType.Fields, func(f *Field) bool {
448+
if f.ResourceReference == nil {
449+
return false
450+
}
451+
resource, ok := m.Model.State.ResourceByType[f.ResourceReference.Type]
452+
if !ok {
453+
return false
454+
}
455+
return strings.ToLower(resource.Singular) == maybeSingular
456+
})
457+
if fieldIndex == -1 {
458+
return nil
459+
}
460+
461+
return &AIPStandardDeleteInfo{
462+
ResourceNameRequestField: m.InputType.Fields[fieldIndex],
463+
}
464+
}
465+
416466
// PathInfo contains normalized request path information.
417467
type PathInfo struct {
418468
// The list of bindings, including the top-level binding.

internal/sidekick/api/model_test.go

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,20 @@ func TestIsAIPStandard(t *testing.T) {
303303
OutputType: output,
304304
}
305305

306+
validDeleteMethod := &Method{
307+
Name: "DeleteSecret",
308+
InputType: &Message{Name: "DeleteSecretRequest", Fields: []*Field{{Name: "name", ResourceReference: &ResourceReference{Type: resourceType}}}},
309+
ReturnsEmpty: true,
310+
Model: &API{
311+
ResourceDefinitions: []*Resource{resource},
312+
State: &APIState{
313+
ResourceByType: map[string]*Resource{
314+
resourceType: resource,
315+
},
316+
},
317+
},
318+
}
319+
306320
// Setup for an invalid Get operation (e.g., wrong name)
307321
invalidGetMethod := &Method{
308322
Name: "ListSecrets", // Not a Get method
@@ -320,6 +334,11 @@ func TestIsAIPStandard(t *testing.T) {
320334
method: validGetMethod,
321335
want: true,
322336
},
337+
{
338+
name: "standard delete method returns true",
339+
method: validDeleteMethod,
340+
want: true,
341+
},
323342
{
324343
name: "non-standard method returns false",
325344
method: invalidGetMethod,
@@ -475,6 +494,103 @@ func TestAIPStandardGetInfo(t *testing.T) {
475494
}
476495
}
477496

497+
func TestAIPStandardDeleteInfo(t *testing.T) {
498+
resourceType := "google.cloud.secretmanager.v1/Secret"
499+
resourceNameField := &Field{
500+
Name: "name",
501+
ResourceReference: &ResourceReference{
502+
Type: resourceType,
503+
},
504+
}
505+
resource := &Resource{
506+
Type: resourceType,
507+
Singular: "secret",
508+
}
509+
model := &API{
510+
ResourceDefinitions: []*Resource{resource},
511+
State: &APIState{
512+
ResourceByType: map[string]*Resource{
513+
resourceType: resource,
514+
},
515+
},
516+
}
517+
518+
testCases := []struct {
519+
name string
520+
method *Method
521+
want *AIPStandardDeleteInfo
522+
}{
523+
{
524+
name: "valid simple delete",
525+
method: &Method{
526+
Name: "DeleteSecret",
527+
InputType: &Message{Name: "DeleteSecretRequest", Fields: []*Field{resourceNameField}},
528+
ReturnsEmpty: true,
529+
Model: model,
530+
},
531+
want: &AIPStandardDeleteInfo{
532+
ResourceNameRequestField: resourceNameField,
533+
},
534+
},
535+
{
536+
name: "valid lro delete",
537+
method: &Method{
538+
Name: "DeleteSecret",
539+
InputType: &Message{Name: "DeleteSecretRequest", Fields: []*Field{resourceNameField}},
540+
OperationInfo: &OperationInfo{},
541+
Model: model,
542+
},
543+
want: &AIPStandardDeleteInfo{
544+
ResourceNameRequestField: resourceNameField,
545+
},
546+
},
547+
{
548+
name: "incorrect method name",
549+
method: &Method{
550+
Name: "RemoveSecret",
551+
InputType: &Message{Name: "DeleteSecretRequest", Fields: []*Field{resourceNameField}},
552+
Model: model,
553+
},
554+
want: nil,
555+
},
556+
{
557+
name: "incorrect request name",
558+
method: &Method{
559+
Name: "DeleteSecret",
560+
InputType: &Message{Name: "RemoveSecretRequest", Fields: []*Field{resourceNameField}},
561+
Model: model,
562+
},
563+
want: nil,
564+
},
565+
{
566+
name: "resource not found in ResourceByType map",
567+
method: &Method{
568+
Name: "DeleteSecret",
569+
InputType: &Message{
570+
Name: "DeleteSecretRequest",
571+
Fields: []*Field{
572+
{
573+
Name: "name",
574+
ResourceReference: &ResourceReference{Type: "nonexistent.googleapis.com/NonExistent"},
575+
},
576+
},
577+
},
578+
Model: model, // model's ResourceByType does not contain the nonexistent resource
579+
},
580+
want: nil,
581+
},
582+
}
583+
584+
for _, tc := range testCases {
585+
t.Run(tc.name, func(t *testing.T) {
586+
got := tc.method.AIPStandardDeleteInfo()
587+
if diff := cmp.Diff(tc.want, got); diff != "" {
588+
t.Errorf("AIPStandardDeleteInfo() mismatch (-want +got):\n%s", diff)
589+
}
590+
})
591+
}
592+
}
593+
478594
func TestFieldTypePredicates(t *testing.T) {
479595
type TestCase struct {
480596
field *Field

internal/sidekick/parser/protobuf.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -187,10 +187,11 @@ func makeAPIForProtobuf(serviceConfig *serviceconfig.Service, req *pluginpb.Code
187187
enabledMixinMethods mixinMethods = make(map[string]bool)
188188
)
189189
state := &api.APIState{
190-
ServiceByID: make(map[string]*api.Service),
191-
MethodByID: make(map[string]*api.Method),
192-
MessageByID: make(map[string]*api.Message),
193-
EnumByID: make(map[string]*api.Enum),
190+
ServiceByID: make(map[string]*api.Service),
191+
MethodByID: make(map[string]*api.Method),
192+
MessageByID: make(map[string]*api.Message),
193+
EnumByID: make(map[string]*api.Enum),
194+
ResourceByType: make(map[string]*api.Resource),
194195
}
195196
result := &api.API{
196197
State: state,
@@ -488,7 +489,7 @@ func processMessage(state *api.APIState, m *descriptorpb.DescriptorProto, mFQN,
488489
if opts.GetMapEntry() {
489490
message.IsMap = true
490491
}
491-
if err := processResourceAnnotation(opts, message); err != nil {
492+
if err := processResourceAnnotation(opts, message, state); err != nil {
492493
return nil, err
493494
}
494495
}
@@ -553,7 +554,7 @@ func processMessage(state *api.APIState, m *descriptorpb.DescriptorProto, mFQN,
553554
return message, nil
554555
}
555556

556-
func processResourceAnnotation(opts *descriptorpb.MessageOptions, message *api.Message) error {
557+
func processResourceAnnotation(opts *descriptorpb.MessageOptions, message *api.Message, state *api.APIState) error {
557558
if !proto.HasExtension(opts, annotations.E_Resource) {
558559
return nil
559560
}
@@ -568,13 +569,15 @@ func processResourceAnnotation(opts *descriptorpb.MessageOptions, message *api.M
568569
return fmt.Errorf("in message %q: %w", message.ID, err)
569570
}
570571

571-
message.Resource = &api.Resource{
572+
resource := &api.Resource{
572573
Type: res.GetType(),
573574
Patterns: patterns,
574575
Plural: res.GetPlural(),
575576
Singular: res.GetSingular(),
576577
Self: message,
577578
}
579+
message.Resource = resource
580+
state.ResourceByType[resource.Type] = resource
578581
return nil
579582
}
580583

internal/sidekick/parser/protobuf_test.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1891,6 +1891,23 @@ func TestProtobuf_ResourceAnnotations(t *testing.T) {
18911891
}
18921892
})
18931893

1894+
t.Run("API.State.ResourceByType", func(t *testing.T) {
1895+
if _, ok := test.State.ResourceByType["library.googleapis.com/Shelf"]; ok {
1896+
t.Errorf("Resource 'library.googleapis.com/Shelf' should not be in ResourceByType map")
1897+
}
1898+
1899+
bookResource, ok := test.State.ResourceByType["library.googleapis.com/Book"]
1900+
if !ok {
1901+
t.Fatalf("Expected resource 'library.googleapis.com/Book' not found in ResourceByType map")
1902+
}
1903+
if bookResource.Type != "library.googleapis.com/Book" {
1904+
t.Errorf("bookResource.Type = %q; want %q", bookResource.Type, "library.googleapis.com/Book")
1905+
}
1906+
if bookResource.Self.Name != "Book" {
1907+
t.Errorf("bookResource.Self.Name = %q; want %q", bookResource.Self.Name, "Book")
1908+
}
1909+
})
1910+
18941911
t.Run("Message.Resource", func(t *testing.T) {
18951912
bookMessage, ok := test.State.MessageByID[".test.Book"]
18961913
if !ok {

internal/sidekick/rust/templates/common/client_method_samples/builder_fields.mustache

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ limitations under the License.
1616
{{#AIPStandardGetInfo}}
1717
/// .set_{{ResourceNameRequestField.Codec.SetterName}}(resource_name)
1818
{{/AIPStandardGetInfo}}
19+
{{#AIPStandardDeleteInfo}}
20+
/// .set_{{ResourceNameRequestField.Codec.SetterName}}(resource_name)
21+
{{/AIPStandardDeleteInfo}}
1922
{{^IsAIPStandard}}
2023
/// /* set fields */
2124
{{/IsAIPStandard}}

internal/sidekick/rust/templates/common/client_method_samples/parameters.mustache

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ limitations under the License.
1717
/// client: &{{Service.Codec.Name}},
1818
/// resource_name: &str
1919
{{/AIPStandardGetInfo}}
20+
{{#AIPStandardDeleteInfo}}
21+
/// client: &{{Service.Codec.Name}},
22+
/// resource_name: &str
23+
{{/AIPStandardDeleteInfo}}
2024
{{^IsAIPStandard}}
2125
/// client: &{{Service.Codec.Name}}
2226
{{/IsAIPStandard}}

0 commit comments

Comments
 (0)