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