Skip to content

Commit

Permalink
runtime: add support for time types in query parameters (#693)
Browse files Browse the repository at this point in the history
Adds support for parsing the google.protobuf.Duration as
well as native *time.Time and *time.Duration types in
url query parameters.

Helps #400.
  • Loading branch information
johanbrandhorst authored and tmc committed Jul 5, 2018
1 parent e93c623 commit 5948c50
Show file tree
Hide file tree
Showing 2 changed files with 100 additions and 1 deletion.
37 changes: 36 additions & 1 deletion runtime/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,23 @@ func populateField(f reflect.Value, value string, props *proto.Properties) error
f.Field(0).SetInt(int64(t.Unix()))
f.Field(1).SetInt(int64(t.Nanosecond()))
return nil
case "Duration":
if value == "null" {
f.Field(0).SetInt(0)
f.Field(1).SetInt(0)
return nil
}
d, err := time.ParseDuration(value)
if err != nil {
return fmt.Errorf("bad Duration: %v", err)
}

ns := d.Nanoseconds()
s := ns / 1e9
ns %= 1e9
f.Field(0).SetInt(s)
f.Field(1).SetInt(ns)
return nil
case "DoubleValue":
fallthrough
case "FloatValue":
Expand Down Expand Up @@ -284,14 +301,32 @@ func populateField(f reflect.Value, value string, props *proto.Properties) error
}
}

// Handle Time and Duration stdlib types
switch t := i.(type) {
case *time.Time:
pt, err := time.Parse(time.RFC3339Nano, value)
if err != nil {
return fmt.Errorf("bad Timestamp: %v", err)
}
*t = pt
return nil
case *time.Duration:
d, err := time.ParseDuration(value)
if err != nil {
return fmt.Errorf("bad Duration: %v", err)
}
*t = d
return nil
}

// is the destination field an enumeration type?
if enumValMap := proto.EnumValueMap(props.Enum); enumValMap != nil {
return populateFieldEnum(f, value, enumValMap)
}

conv, ok := convFromType[f.Kind()]
if !ok {
return fmt.Errorf("unsupported field type %T", f)
return fmt.Errorf("field type %T is not supported in query parameters", i)
}
result := conv.Call([]reflect.Value{reflect.ValueOf(value)})
if err := result[1].Interface(); err != nil {
Expand Down
64 changes: 64 additions & 0 deletions runtime/query_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/golang/protobuf/proto"
"github.com/golang/protobuf/ptypes"
"github.com/golang/protobuf/ptypes/duration"
"github.com/golang/protobuf/ptypes/timestamp"
"github.com/golang/protobuf/ptypes/wrappers"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
Expand All @@ -25,6 +26,10 @@ func TestPopulateParameters(t *testing.T) {
t.Fatalf("Couldn't setup timestamp in Protobuf format: %v", err)
}

durationT := 13 * time.Hour
durationStr := durationT.String()
durationPb := ptypes.DurationProto(durationT)

fieldmaskStr := "float_value,double_value"
fieldmaskPb := &field_mask.FieldMask{Paths: []string{"float_value", "double_value"}}

Expand All @@ -49,6 +54,7 @@ func TestPopulateParameters(t *testing.T) {
"enum_value": {"1"},
"repeated_enum": {"1", "2", "0"},
"timestamp_value": {timeStr},
"duration_value": {durationStr},
"fieldmask_value": {fieldmaskStr},
"wrapper_float_value": {"1.5"},
"wrapper_double_value": {"2.5"},
Expand Down Expand Up @@ -94,6 +100,7 @@ func TestPopulateParameters(t *testing.T) {
EnumValue: EnumValue_Y,
RepeatedEnum: []EnumValue{EnumValue_Y, EnumValue_Z, EnumValue_X},
TimestampValue: timePb,
DurationValue: durationPb,
FieldMaskValue: fieldmaskPb,
WrapperFloatValue: &wrappers.FloatValue{Value: 1.5},
WrapperDoubleValue: &wrappers.DoubleValue{Value: 2.5},
Expand Down Expand Up @@ -142,6 +149,7 @@ func TestPopulateParameters(t *testing.T) {
"enumValue": {"1"},
"repeatedEnum": {"1", "2", "0"},
"timestampValue": {timeStr},
"durationValue": {durationStr},
"fieldmaskValue": {fieldmaskStr},
"wrapperFloatValue": {"1.5"},
"wrapperDoubleValue": {"2.5"},
Expand All @@ -168,6 +176,7 @@ func TestPopulateParameters(t *testing.T) {
EnumValue: EnumValue_Y,
RepeatedEnum: []EnumValue{EnumValue_Y, EnumValue_Z, EnumValue_X},
TimestampValue: timePb,
DurationValue: durationPb,
FieldMaskValue: fieldmaskPb,
WrapperFloatValue: &wrappers.FloatValue{Value: 1.5},
WrapperDoubleValue: &wrappers.DoubleValue{Value: 2.5},
Expand Down Expand Up @@ -338,6 +347,51 @@ func TestPopulateParameters(t *testing.T) {
}
}

func TestPopulateParametersWithNativeTypes(t *testing.T) {
timeT := time.Date(2016, time.December, 15, 12, 23, 32, 49, time.UTC)
timeStr := timeT.Format(time.RFC3339Nano)

durationT := 13 * time.Hour
durationStr := durationT.String()

for _, spec := range []struct {
values url.Values
want *nativeProto3Message
}{
{
values: url.Values{
"native_timestamp_value": {timeStr},
"native_duration_value": {durationStr},
},
want: &nativeProto3Message{
NativeTimeValue: &timeT,
NativeDurationValue: &durationT,
},
},
{
values: url.Values{
"nativeTimestampValue": {timeStr},
"nativeDurationValue": {durationStr},
},
want: &nativeProto3Message{
NativeTimeValue: &timeT,
NativeDurationValue: &durationT,
},
},
} {
msg := new(nativeProto3Message)
err := runtime.PopulateQueryParameters(msg, spec.values, utilities.NewDoubleArray(nil))

if err != nil {
t.Errorf("runtime.PopulateQueryParameters(msg, %v, utilities.NewDoubleArray(nil)) failed with %v; want success", spec.values, err)
continue
}
if got, want := msg, spec.want; !proto.Equal(got, want) {
t.Errorf("runtime.PopulateQueryParameters(msg, %v, utilities.NewDoubleArray(nil)) = %v; want %v", spec.values, got, want)
}
}
}

func TestPopulateParametersWithFilters(t *testing.T) {
for _, spec := range []struct {
values url.Values
Expand Down Expand Up @@ -537,6 +591,7 @@ type proto3Message struct {
EnumValue EnumValue `protobuf:"varint,11,opt,name=enum_value,json=enumValue,enum=runtime_test_api.EnumValue" json:"enum_value,omitempty"`
RepeatedEnum []EnumValue `protobuf:"varint,12,rep,packed,name=repeated_enum,json=repeatedEnum,enum=runtime_test_api.EnumValue" json:"repeated_enum,omitempty"`
TimestampValue *timestamp.Timestamp `protobuf:"bytes,16,opt,name=timestamp_value,json=timestampValue" json:"timestamp_value,omitempty"`
DurationValue *duration.Duration `protobuf:"bytes,42,opt,name=duration_value,json=durationValue" json:"duration_value,omitempty"`
FieldMaskValue *field_mask.FieldMask `protobuf:"bytes,27,opt,name=fieldmask_value,json=fieldmaskValue" json:"fieldmask_value,omitempty"`
OneofValue proto3Message_OneofValue `protobuf_oneof:"oneof_value"`
WrapperDoubleValue *wrappers.DoubleValue `protobuf:"bytes,17,opt,name=wrapper_double_value,json=wrapperDoubleValue" json:"wrapper_double_value,omitempty"`
Expand Down Expand Up @@ -680,6 +735,15 @@ func _proto3Message_OneofSizer(msg proto.Message) (n int) {
return n
}

type nativeProto3Message struct {
NativeTimeValue *time.Time `protobuf:"bytes,1,opt,name=native_timestamp_value,json=nativeTimestampValue" json:"native_timestamp_value,omitempty"`
NativeDurationValue *time.Duration `protobuf:"bytes,2,opt,name=native_duration_value,json=nativeDurationValue" json:"native_duration_value,omitempty"`
}

func (m *nativeProto3Message) Reset() { *m = nativeProto3Message{} }
func (m *nativeProto3Message) String() string { return proto.CompactTextString(m) }
func (*nativeProto3Message) ProtoMessage() {}

type proto2Message struct {
Nested *proto3Message `protobuf:"bytes,1,opt,name=nested,json=nested" json:"nested,omitempty"`
FloatValue *float32 `protobuf:"fixed32,2,opt,name=float_value,json=floatValue" json:"float_value,omitempty"`
Expand Down

0 comments on commit 5948c50

Please sign in to comment.