From 11fd93f07f5501ece6f6ca81a32de0031a8c2978 Mon Sep 17 00:00:00 2001 From: Johan Brandhorst Date: Tue, 3 Jul 2018 21:48:34 +0100 Subject: [PATCH] runtime: add support for time types in query parameters Adds support for parsing the google.protobuf.Duration as well as native *time.Time and *time.Duration types in url query parameters. Helps #400. --- runtime/query.go | 37 ++++++++++++++++++++++++- runtime/query_test.go | 64 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+), 1 deletion(-) diff --git a/runtime/query.go b/runtime/query.go index 82c926a95f3..bb9359f17c0 100644 --- a/runtime/query.go +++ b/runtime/query.go @@ -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": @@ -284,6 +301,24 @@ 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) @@ -291,7 +326,7 @@ func populateField(f reflect.Value, value string, props *proto.Properties) error 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 { diff --git a/runtime/query_test.go b/runtime/query_test.go index 904be49920a..358847928c8 100644 --- a/runtime/query_test.go +++ b/runtime/query_test.go @@ -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" @@ -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"}} @@ -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"}, @@ -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}, @@ -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"}, @@ -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}, @@ -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 @@ -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"` @@ -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"`