Skip to content

Commit

Permalink
fix: use resource field in http body for Updates (#1198)
Browse files Browse the repository at this point in the history
fix: support parsing well known types in query params

Changing the google.api.http body field is a breaking change but we don't have a major version in this project yet, nor do we need to take one for this.
  • Loading branch information
noahdietz committed Sep 1, 2022
1 parent cdb4ce6 commit 48a2632
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 7 deletions.
2 changes: 1 addition & 1 deletion schema/google/showcase/v1beta1/identity.proto
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ service Identity {
rpc UpdateUser(UpdateUserRequest) returns (User) {
option (google.api.http) = {
patch: "/v1beta1/{user.name=users/*}"
body: "*"
body: "user"
};
}

Expand Down
6 changes: 3 additions & 3 deletions schema/google/showcase/v1beta1/messaging.proto
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ service Messaging {
rpc UpdateRoom(UpdateRoomRequest) returns (Room) {
option (google.api.http) = {
patch: "/v1beta1/{room.name=rooms/*}"
body: "*"
body: "room"
};
}

Expand Down Expand Up @@ -109,10 +109,10 @@ service Messaging {
rpc UpdateBlurb(UpdateBlurbRequest) returns (Blurb) {
option (google.api.http) = {
patch: "/v1beta1/{blurb.name=rooms/*/blurbs/*}"
body: "*"
body: "blurb"
additional_bindings: {
patch: "/v1beta1/{blurb.name=users/*/profile/blurbs/*}"
body: "*"
body: "blurb"
}
};
}
Expand Down
2 changes: 1 addition & 1 deletion util/genrest/gomodelcreator.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ func NewGoModel(protoModel *protomodel.Model) (*gomodel.Model, error) {
if bodyFieldDesc == nil {
goModel.AccumulateError(fmt.Errorf("could not find body field %q in %q", binding.BodyField, inProtoType.GetName()))
}
bodyFieldTypeDesc, ok := protoInfo.Type[*bodyFieldDesc.TypeName]
bodyFieldTypeDesc, ok := protoInfo.Type[bodyFieldDesc.GetTypeName()]
if !ok {
goModel.AccumulateError(fmt.Errorf("could not read protoInfo[%q]", inProtoType))
}
Expand Down
53 changes: 51 additions & 2 deletions util/genrest/resttools/populatefield.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,31 @@ import (
"strconv"
"strings"

"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
)

var wellKnownTypes = map[string]bool{
// == The following are the only three common types used in this API ==
"google.protobuf.FieldMask": true,
"google.protobuf.Timestamp": true,
"google.protobuf.Duration": true,
// == End utilized types ==
"google.protobuf.DoubleValue": true,
"google.protobuf.FloatValue": true,
"google.protobuf.Int64Value": true,
"google.protobuf.UInt64Value": true,
"google.protobuf.Int32Value": true,
"google.protobuf.UInt32Value": true,
"google.protobuf.BoolValue": true,
"google.protobuf.StringValue": true,
"google.protobuf.BytesValue": true,
// TODO: Determine if the following are even viable as query params.
"google.protobuf.Value": true,
"google.protobuf.ListValue": true,
}

// PopulateSingularFields sets the fields within protoMessage to the values provided in
// fieldValues. The fields and values are provided as a map of field paths to the string
// representation of their values. The field paths can refer to fields nested arbitrarily deep
Expand Down Expand Up @@ -113,8 +134,14 @@ func PopulateOneField(protoMessage proto.Message, fieldPath string, fieldValues
kind := fieldDescriptor.Kind()
switch kind {
case protoreflect.MessageKind:
parseError = fmt.Errorf("terminal field %q of field path %q in message %q is a message type",
fieldName, fieldPath, messageFullName)
if pval, err := parseWellKnownType(message, fieldDescriptor, value); err != nil {
parseError = err
} else if pval != nil {
protoValue = *pval
} else {
parseError = fmt.Errorf("terminal field %q of field path %q in message %q is a message type",
fieldName, fieldPath, messageFullName)
}

// reference for proto scalar types:
// https://developers.google.com/protocol-buffers/docs/proto3#scalar
Expand Down Expand Up @@ -214,3 +241,25 @@ func findProtoFieldByJSONName(fieldName string, fields protoreflect.FieldDescrip
}
return result, nil
}

func parseWellKnownType(message protoreflect.Message, fieldDescriptor protoreflect.FieldDescriptor, value string) (*protoreflect.Value, error) {
messageFieldTypes, ok := message.Type().(protoreflect.MessageFieldTypes)
if !ok {
return nil, fmt.Errorf("expected message to implement protoreflect.MessageFieldTypes but it did not")
}
fieldMsg := messageFieldTypes.Message(fieldDescriptor.Index())
fullName := string(fieldMsg.Descriptor().FullName())
if !wellKnownTypes[fullName] {
return nil, nil
}

msgValue := fieldMsg.New()
err := protojson.Unmarshal([]byte(value), msgValue.Interface())
if err != nil {
return nil, fmt.Errorf("unable to unmarshal field %q in message %q: %w",
string(fieldDescriptor.Name()), string(message.Descriptor().FullName()), err)
}

v := protoreflect.ValueOfMessage(msgValue)
return &v, nil
}
52 changes: 52 additions & 0 deletions util/genrest/resttools/populatefield_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,63 @@ package resttools
import (
"reflect"
"testing"
"time"

"github.com/google/go-cmp/cmp"
genprotopb "github.com/googleapis/gapic-showcase/server/genproto"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/encoding/prototext"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"google.golang.org/protobuf/types/known/durationpb"
"google.golang.org/protobuf/types/known/fieldmaskpb"
"google.golang.org/protobuf/types/known/timestamppb"
)

func TestParseWellKnownType(t *testing.T) {
for _, tst := range []struct {
name string
msg protoreflect.Message
field protoreflect.Name
want proto.Message
}{
{
"google.protobuf.FieldMask",
(&genprotopb.UpdateUserRequest{}).ProtoReflect(),
"update_mask",
&fieldmaskpb.FieldMask{Paths: []string{"foo", "bar", "baz"}},
},
{
"google.protobuf.Timestamp",
(&genprotopb.User{}).ProtoReflect(),
"create_time",
timestamppb.Now(),
},
{
"google.protobuf.Duration",
(&genprotopb.Sequence_Response{}).ProtoReflect(),
"delay",
durationpb.New(5 * time.Second),
},
} {
data, _ := protojson.Marshal(tst.want)
value := string(data)
fd := tst.msg.Descriptor().Fields().ByName(tst.field)

gotp, err := parseWellKnownType(tst.msg, fd, value)
if err != nil {
t.Fatal(err)
}
if gotp == nil {
t.Fatal("expected non-nil value from parsing")
}
got := gotp.Message().Interface()
if diff := cmp.Diff(got, tst.want, cmp.Comparer(proto.Equal)); diff != "" {
t.Fatalf("%s: got(-),want(+):\n%s", "FieldMask", diff)
}
}
}

func TestPopulateOneFieldError(t *testing.T) {
for idx, testCase := range []struct {
field string
Expand Down

0 comments on commit 48a2632

Please sign in to comment.