Skip to content

Commit

Permalink
Add a time type for use in APIs. (#9911)
Browse files Browse the repository at this point in the history
* Add a time type for use in APIs.
* go mod vendor
  • Loading branch information
Mark Gritter authored Sep 9, 2020
1 parent 0ff7ce9 commit de9e019
Show file tree
Hide file tree
Showing 10 changed files with 255 additions and 4 deletions.
2 changes: 2 additions & 0 deletions sdk/framework/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,8 @@ func (t FieldType) Zero() interface{} {
return http.Header{}
case TypeFloat:
return 0.0
case TypeTime:
return time.Time{}
default:
panic("unknown type: " + t.String())
}
Expand Down
17 changes: 15 additions & 2 deletions sdk/framework/field_data.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,7 @@ func (d *FieldData) Validate() error {
switch schema.Type {
case TypeBool, TypeInt, TypeMap, TypeDurationSecond, TypeSignedDurationSecond, TypeString,
TypeLowerCaseString, TypeNameString, TypeSlice, TypeStringSlice, TypeCommaStringSlice,
TypeKVPairs, TypeCommaIntSlice, TypeHeader, TypeFloat:
TypeKVPairs, TypeCommaIntSlice, TypeHeader, TypeFloat, TypeTime:
_, _, err := d.getPrimitive(field, schema)
if err != nil {
return errwrap.Wrapf(fmt.Sprintf("error converting input %v for field %q: {{err}}", value, field), err)
Expand Down Expand Up @@ -133,7 +133,7 @@ func (d *FieldData) GetOkErr(k string) (interface{}, bool, error) {
switch schema.Type {
case TypeBool, TypeInt, TypeMap, TypeDurationSecond, TypeSignedDurationSecond, TypeString,
TypeLowerCaseString, TypeNameString, TypeSlice, TypeStringSlice, TypeCommaStringSlice,
TypeKVPairs, TypeCommaIntSlice, TypeHeader, TypeFloat:
TypeKVPairs, TypeCommaIntSlice, TypeHeader, TypeFloat, TypeTime:
return d.getPrimitive(k, schema)
default:
return nil, false,
Expand Down Expand Up @@ -221,6 +221,19 @@ func (d *FieldData) getPrimitive(k string, schema *FieldSchema) (interface{}, bo
}
return result, true, nil

case TypeTime:
switch inp := raw.(type) {
case nil:
// Handle nil interface{} as a non-error case
return nil, false, nil
default:
time, err := parseutil.ParseAbsoluteTime(inp)
if err != nil {
return nil, false, err
}
return time, true, nil
}

case TypeCommaIntSlice:
var result []int
config := &mapstructure.DecoderConfig{
Expand Down
35 changes: 35 additions & 0 deletions sdk/framework/field_data_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"net/http"
"reflect"
"testing"
"time"
)

func TestFieldDataGet(t *testing.T) {
Expand Down Expand Up @@ -931,6 +932,40 @@ func TestFieldDataGet(t *testing.T) {
0.0,
true,
},

"type time, not supplied": {
map[string]*FieldSchema{
"foo": {Type: TypeTime},
},
map[string]interface{}{},
"foo",
time.Time{},
false,
},
"type time, string value": {
map[string]*FieldSchema{
"foo": {Type: TypeTime},
},
map[string]interface{}{
"foo": "2021-12-11T09:08:07Z",
},
"foo",
// Comparison uses DeepEqual() so better match exactly,
// can't have a different location.
time.Date(2021, 12, 11, 9, 8, 7, 0, time.UTC),
false,
},
"type time, invalid value": {
map[string]*FieldSchema{
"foo": {Type: TypeTime},
},
map[string]interface{}{
"foo": "2021-13-11T09:08:07+02:00",
},
"foo",
time.Time{},
true,
},
}

for name, tc := range cases {
Expand Down
5 changes: 5 additions & 0 deletions sdk/framework/field_type.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@ const (

// TypeFloat parses both float32 and float64 values
TypeFloat

// TypeTime represents absolute time, using an RFC3999 format on the wire
TypeTime
)

func (t FieldType) String() string {
Expand All @@ -82,6 +85,8 @@ func (t FieldType) String() string {
return "header"
case TypeFloat:
return "float"
case TypeTime:
return "time"
default:
return "unknown type"
}
Expand Down
48 changes: 48 additions & 0 deletions sdk/helper/parseutil/parseutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,54 @@ func ParseDurationSecond(in interface{}) (time.Duration, error) {
return dur, nil
}

func ParseAbsoluteTime(in interface{}) (time.Time, error) {
var t time.Time
switch inp := in.(type) {
case nil:
// return default of zero
return t, nil
case string:
// Allow RFC3339 with nanoseconds, or without,
// or an epoch time as an integer.
var err error
t, err = time.Parse(time.RFC3339Nano, inp)
if err == nil {
break
}
t, err = time.Parse(time.RFC3339, inp)
if err == nil {
break
}
epochTime, err := strconv.ParseInt(inp, 10, 64)
if err == nil {
t = time.Unix(epochTime, 0)
break
}
return t, errors.New("could not parse string as date and time")
case json.Number:
epochTime, err := inp.Int64()
if err != nil {
return t, err
}
t = time.Unix(epochTime, 0)
case int:
t = time.Unix(int64(inp), 0)
case int32:
t = time.Unix(int64(inp), 0)
case int64:
t = time.Unix(inp, 0)
case uint:
t = time.Unix(int64(inp), 0)
case uint32:
t = time.Unix(int64(inp), 0)
case uint64:
t = time.Unix(int64(inp), 0)
default:
return t, errors.New("could not parse time from input type")
}
return t, nil
}

func ParseInt(in interface{}) (int64, error) {
var ret int64
jsonIn, ok := in.(json.Number)
Expand Down
80 changes: 80 additions & 0 deletions sdk/helper/parseutil/parseutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,86 @@ func Test_ParseDurationSecond(t *testing.T) {
}
}

func Test_ParseAbsoluteTime(t *testing.T) {
testCases := []struct {
inp interface{}
valid bool
expected time.Time
}{
{
"2020-12-11T09:08:07.654321Z",
true,
time.Date(2020, 12, 11, 9, 8, 7, 654321000, time.UTC),
},
{
"2020-12-11T09:08:07+02:00",
true,
time.Date(2020, 12, 11, 7, 8, 7, 0, time.UTC),
},
{
"2021-12-11T09:08:07Z",
true,
time.Date(2021, 12, 11, 9, 8, 7, 0, time.UTC),
},
{
"2021-12-11T09:08:07",
false,
time.Time{},
},
{
"1670749687",
true,
time.Date(2022, 12, 11, 9, 8, 7, 0, time.UTC),
},
{
1670749687,
true,
time.Date(2022, 12, 11, 9, 8, 7, 0, time.UTC),
},
{
uint32(1670749687),
true,
time.Date(2022, 12, 11, 9, 8, 7, 0, time.UTC),
},
{
json.Number("1670749687"),
true,
time.Date(2022, 12, 11, 9, 8, 7, 0, time.UTC),
},
{
nil,
true,
time.Time{},
},
{
struct{}{},
false,
time.Time{},
},
{
true,
false,
time.Time{},
},
}
for _, tc := range testCases {
outp, err := ParseAbsoluteTime(tc.inp)
if err != nil {
if tc.valid {
t.Errorf("failed to parse: %v", tc.inp)
}
continue
}
if err == nil && !tc.valid {
t.Errorf("no error for: %v", tc.inp)
continue
}
if !outp.Equal(tc.expected) {
t.Errorf("input %v parsed as %v, expected %v", tc.inp, outp, tc.expected)
}
}
}

func Test_ParseBool(t *testing.T) {
outp, err := ParseBool("true")
if err != nil {
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit de9e019

Please sign in to comment.