Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a time type for use in APIs. #9911

Merged
merged 3 commits into from
Sep 9, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.