feat(subtypes): Add new transformation registration option

This allows the user to register a custom transformation callback
for a specific protobuf message, in cases where the transformation
is sufficiently complex for it to be cumbersome to formalise
the logic through protobuf options.
This commit is contained in:
Johan Brandhorst-Satzkorn committed 2022-04-20 13:32:31 -04:00
1 parent 1fd44ee0f1
commit 6d56fcd26f
5 files changed
+609 -281

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -40,6 +40,16 @@ message TestNoOneOf {
google.protobuf.Struct attributes = 10;
}
message TestCustomTransformation {
string some_random_id = 1;
string secondary_id = 2;
oneof attrs {
google.protobuf.Struct attributes = 10;
TestSubResourceAttributes sub_resource_attributes = 20;
}
}
service TestResourceService {
rpc TestListResource(TestListResourceRequest) returns (TestListResourceResponse) {
option (google.api.http) = {
+18 -2
View File
@@ -172,6 +172,14 @@ func transformResponseItemAttributes(item proto.Message) error {
return convertAttributesToDefault(item, st)
}
func transformRequest(msg proto.Message) error {
fqn := msg.ProtoReflect().Descriptor().FullName()
if fn, ok := globalTransformationRegistry.requestTransformationFuncs[fqn]; ok {
return fn(msg)
}
return transformRequestAttributes(msg)
}
// transformResponseAttributes will modify the response proto.Message, setting
// any subtype attribute fields into the default structpb.Struct field. It looks
// for some specific structure in the proto.Message to identify that it is a
@@ -232,6 +240,14 @@ func transformResponseAttributes(res proto.Message) error {
return nil
}
func transformResponse(msg proto.Message) error {
fqn := msg.ProtoReflect().Descriptor().FullName()
if fn, ok := globalTransformationRegistry.responseTransformationFuncs[fqn]; ok {
return fn(msg)
}
return transformResponseAttributes(msg)
}
// AttributeTransformerInterceptor is a grpc server interceptor that will
// transform subtype attributes for requests and responses. This will only
// modify requests and responses that adhere to a specific structure and is done
@@ -280,7 +296,7 @@ func AttributeTransformerInterceptor(_ context.Context) grpc.UnaryServerIntercep
const op = "subtypes.AttributeTransformInterceptor"
return func(interceptorCtx context.Context, req interface{}, _ *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
if reqMsg, ok := req.(proto.Message); ok {
if err := transformRequestAttributes(reqMsg); err != nil {
if err := transformRequest(reqMsg); err != nil {
return nil, handlers.InvalidArgumentErrorf("Error in provided request.",
map[string]string{"attributes": "Attribute fields do not match the expected format."})
}
@@ -289,7 +305,7 @@ func AttributeTransformerInterceptor(_ context.Context) grpc.UnaryServerIntercep
res, handlerErr := handler(interceptorCtx, req)
if res, ok := res.(proto.Message); ok {
if err := transformResponseAttributes(res); err != nil {
if err := transformResponse(res); err != nil {
return nil, handlers.ApiErrorWithCodeAndMessage(codes.Internal, "failed building attribute struct: %v", err)
}
}
@@ -588,3 +588,90 @@ func TestTransformResponseAttributes(t *testing.T) {
})
}
}
func TestCustomTransformRequest(t *testing.T) {
RegisterRequestTransformationFunc(
&attribute.TestCustomTransformation{},
func(m proto.Message) error {
msg, ok := m.(*attribute.TestCustomTransformation)
require.True(t, ok, "wrong message passed to request transformation callback")
if msg.SomeRandomId == "some_random_id" && msg.SecondaryId == "secondary_id" {
newAttrs := &attribute.TestSubResourceAttributes{}
err := structToProto(msg.GetAttributes(), newAttrs)
require.NoError(t, err)
msg.Attrs = &attribute.TestCustomTransformation_SubResourceAttributes{
SubResourceAttributes: newAttrs,
}
}
return nil
},
)
request := &attribute.TestCustomTransformation{
SomeRandomId: "some_random_id",
SecondaryId: "secondary_id",
Attrs: &attribute.TestCustomTransformation_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
}
expected := &attribute.TestCustomTransformation{
SomeRandomId: "some_random_id",
SecondaryId: "secondary_id",
Attrs: &attribute.TestCustomTransformation_SubResourceAttributes{
SubResourceAttributes: &attribute.TestSubResourceAttributes{
Name: "test",
},
},
}
err := transformRequest(request)
require.NoError(t, err)
assert.Empty(t, cmp.Diff(request, expected, protocmp.Transform()))
}
func TestCustomTransformResponse(t *testing.T) {
RegisterResponseTransformationFunc(
&attribute.TestCustomTransformation{},
func(m proto.Message) error {
msg, ok := m.(*attribute.TestCustomTransformation)
require.True(t, ok, "wrong message passed to response transformation callback")
if msg.SomeRandomId == "some_random_id" && msg.SecondaryId == "secondary_id" {
newAttrs, err := protoToStruct(msg.GetSubResourceAttributes())
require.NoError(t, err)
msg.Attrs = &attribute.TestCustomTransformation_Attributes{
Attributes: newAttrs,
}
}
return nil
},
)
response := &attribute.TestCustomTransformation{
SomeRandomId: "some_random_id",
SecondaryId: "secondary_id",
Attrs: &attribute.TestCustomTransformation_SubResourceAttributes{
SubResourceAttributes: &attribute.TestSubResourceAttributes{
Name: "test",
},
},
}
expected := &attribute.TestCustomTransformation{
SomeRandomId: "some_random_id",
SecondaryId: "secondary_id",
Attrs: &attribute.TestCustomTransformation_Attributes{
Attributes: func() *structpb.Struct {
attrs, _ := structpb.NewStruct(map[string]interface{}{
"name": "test",
})
return attrs
}(),
},
}
err := transformResponse(response)
require.NoError(t, err)
assert.Empty(t, cmp.Diff(response, expected, protocmp.Transform()))
}
@@ -0,0 +1,83 @@
package subtypes
import (
"context"
"fmt"
"sync"
"github.com/hashicorp/boundary/internal/errors"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
)
var globalTransformationRegistry = &transformationRegistry{
requestTransformationFuncs: make(map[protoreflect.FullName]TransformationFunc),
responseTransformationFuncs: make(map[protoreflect.FullName]TransformationFunc),
}
// transformationRegistry stores registered request and response
// transformation functions.
type transformationRegistry struct {
requestTransformationFuncs map[protoreflect.FullName]TransformationFunc
responseTransformationFuncs map[protoreflect.FullName]TransformationFunc
sync.RWMutex
}
func (r *transformationRegistry) registerRequestTransformationFunc(ctx context.Context, msg proto.Message, transformFn TransformationFunc) error {
r.Lock()
defer r.Unlock()
fqn := msg.ProtoReflect().Descriptor().FullName()
const op = "subtypes.(transformationRegistry).RegisterRequestTransformationFunc"
if _, present := r.requestTransformationFuncs[fqn]; present {
return errors.New(
ctx,
errors.SubtypeAlreadyRegistered,
op,
fmt.Sprintf("proto type %q already has a request transformation function registered", fqn),
)
}
r.requestTransformationFuncs[fqn] = transformFn
return nil
}
func (r *transformationRegistry) registerResponseTransformationFunc(ctx context.Context, msg proto.Message, transformFn TransformationFunc) error {
r.Lock()
defer r.Unlock()
fqn := msg.ProtoReflect().Descriptor().FullName()
const op = "subtypes.(transformationRegistry).RegisterResponseTransformationFunc"
if _, present := r.responseTransformationFuncs[fqn]; present {
return errors.New(
ctx,
errors.SubtypeAlreadyRegistered,
op,
fmt.Sprintf("proto type %q already has a response transformation function registered", fqn),
)
}
r.responseTransformationFuncs[fqn] = transformFn
return nil
}
// TransformationFunc defines the signature used to transform
// protobuf message attributes. The proto.Message is mutated in place.
type TransformationFunc func(proto.Message) error
// RegisterRequestTransformationFunc registers a transformation function for
// the provided message. The message should be used as a request parameter to
// a service method. The provided callback is guaranteed to only be called with messages
// of the same type as the provided message.
func RegisterRequestTransformationFunc(msg proto.Message, transformFn TransformationFunc) error {
ctx := context.TODO()
return globalTransformationRegistry.registerRequestTransformationFunc(ctx, msg, transformFn)
}
// RegisterResponseTransformationFunc registers a transformation function for
// the provided message. The message should be used as a response parameter to
// a service method. The provided callback is guaranteed to only be called with messages
// of the same type as the provided message.
func RegisterResponseTransformationFunc(msg proto.Message, transformFn TransformationFunc) error {
ctx := context.TODO()
return globalTransformationRegistry.registerResponseTransformationFunc(ctx, msg, transformFn)
}