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:
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()))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
Reference in new issue
Block a user