feat(subtypes): Add function to convert protobuf for filtering

This will transform a protobuf message with a oneof for attributes into
the API format to support filtering for list endpoints.
This commit is contained in:
Timothy Messier authored and Johan Brandhorst-Satzkorn committed 2022-04-20 13:32:31 -04:00
1 parent 198a39ef1e
commit 3d58ce5f5c
4 files changed
+164 -4

No files matched your search

@@ -130,3 +130,59 @@ func protoToStruct(p proto.Message) (*structpb.Struct, error) {
}
return st, nil
}
// Filterable converts a proto.Message so any subtype attributes fields are
// structed like the API so filter strings will be correctly applied. If the
// given proto.Message does not have any subtype attributes, the original
// proto.Message is returned. To determine if the message needs
// transformation, it looks for a oneof field named "attrs". It also expects
// that there is a structpb.Struct field named "attributes" as part of the
// oneof. Thus the message must be like:
//
// message Foo {
// // other fields
// oneof attrs {
// google.protobuf.Struct attributes = 100;
// // other attribute fields
// }
// }
//
// If the message does not conform to this structure,
// the original message is returned.
func Filterable(item proto.Message) (proto.Message, error) {
clone := proto.Clone(item)
r := clone.ProtoReflect()
attrsField := r.Descriptor().Oneofs().ByName("attrs")
if attrsField == nil {
return item, nil
}
defaultAttrField := attrsField.Fields().ByName("attributes")
if defaultAttrField == nil {
return item, nil
}
var attr proto.Message
var pbAttrs proto.Message
var err error
oneofField := r.WhichOneof(attrsField)
if oneofField == nil {
// attrs field is not set, nothing to do
return item, nil
}
attr = r.Get(oneofField).Message().Interface()
pbAttrs, err = protoToStruct(attr)
if err != nil {
return nil, err
}
r.Set(defaultAttrField, protoreflect.ValueOfMessage(pbAttrs.ProtoReflect()))
f, err := protoToStruct(r.Interface())
if err != nil {
return nil, err
}
return f, nil
}
@@ -0,0 +1,104 @@
package subtypes
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/hashicorp/boundary/internal/gen/testing/attribute"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/testing/protocmp"
"google.golang.org/protobuf/types/known/structpb"
)
func TestFilterable(t *testing.T) {
cases := []struct {
name string
item proto.Message
want proto.Message
}{
{
"TestResource/sub_resource",
&attribute.TestResource{
Type: "sub_resource",
Attrs: &attribute.TestResource_SubResourceAttributes{
SubResourceAttributes: &attribute.TestSubResourceAttributes{
Name: "test",
},
},
},
func() *structpb.Struct {
w, _ := structpb.NewStruct(map[string]interface{}{
"type": "sub_resource",
"attributes": map[string]interface{}{
"name": "test",
},
})
return w
}(),
},
{
"TestResource/default",
&attribute.TestResource{
Type: "default",
Attrs: &attribute.TestResource_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
func() *structpb.Struct {
w, _ := structpb.NewStruct(map[string]interface{}{
"type": "default",
"attributes": map[string]interface{}{
"name": "test",
},
})
return w
}(),
},
{
"TestNoOneOf",
&attribute.TestNoOneOf{
Type: "sub_resource",
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
&attribute.TestNoOneOf{
Type: "sub_resource",
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
},
{
"TestNoAttributes",
&attribute.TestNoAttributes{
Id: "test",
Type: "no_attr",
},
&attribute.TestNoAttributes{
Id: "test",
Type: "no_attr",
},
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, err := Filterable(tc.item)
require.NoError(t, err)
require.Empty(t, cmp.Diff(got, tc.want, protocmp.Transform()))
})
}
}
+2 -2
View File
@@ -46,7 +46,7 @@ var globalAttributeRegistry attributeRegistry
// to be called on any protobuf message. However, if a message has subtypes but
// does not provide a field with a subtype of "default", or if the default
// field is not a *structpb.Struct type, an error is returned.
func (ak attributeRegistry) register(d protoreflect.MessageDescriptor) error {
func (ak *attributeRegistry) register(d protoreflect.MessageDescriptor) error {
ak.Lock()
defer ak.Unlock()
@@ -96,7 +96,7 @@ func (ak attributeRegistry) register(d protoreflect.MessageDescriptor) error {
// attributeField retrieves the FieldDescriptor for a given subtype's
// attribute fields. If the corresponding protobuf message has not been
// registered it will return an error.
func (ak attributeRegistry) attributeField(d protoreflect.MessageDescriptor, t Subtype) (protoreflect.FieldDescriptor, error) {
func (ak *attributeRegistry) attributeField(d protoreflect.MessageDescriptor, t Subtype) (protoreflect.FieldDescriptor, error) {
ak.RLock()
defer ak.RUnlock()
+2 -2
View File
@@ -35,7 +35,7 @@ type sourceRegistry struct {
m map[protoreflect.FullName]protoreflect.FieldDescriptor
}
func (s sourceRegistry) register(d protoreflect.MessageDescriptor) error {
func (s *sourceRegistry) register(d protoreflect.MessageDescriptor) error {
s.Lock()
defer s.Unlock()
@@ -58,7 +58,7 @@ func (s sourceRegistry) register(d protoreflect.MessageDescriptor) error {
return nil
}
func (s sourceRegistry) get(d protoreflect.MessageDescriptor) protoreflect.FieldDescriptor {
func (s *sourceRegistry) get(d protoreflect.MessageDescriptor) protoreflect.FieldDescriptor {
s.RLock()
defer s.RUnlock()