Add a time type for use in APIs. (#9911)

* Add a time type for use in APIs.
* go mod vendor
This commit is contained in:
Mark Gritter
2020-09-09 15:53:51 -05:00
committed by GitHub
parent 0ff7ce975a
commit de9e019088
10 changed files with 255 additions and 4 deletions

View File

@@ -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())
}

View File

@@ -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)
@@ -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,
@@ -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{

View File

@@ -4,6 +4,7 @@ import (
"net/http"
"reflect"
"testing"
"time"
)
func TestFieldDataGet(t *testing.T) {
@@ -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 {

View File

@@ -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 {
@@ -82,6 +85,8 @@ func (t FieldType) String() string {
return "header"
case TypeFloat:
return "float"
case TypeTime:
return "time"
default:
return "unknown type"
}

View File

@@ -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)

View File

@@ -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 {

View File

@@ -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())
}

View File

@@ -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)
@@ -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,
@@ -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{

View File

@@ -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 {
@@ -82,6 +85,8 @@ func (t FieldType) String() string {
return "header"
case TypeFloat:
return "float"
case TypeTime:
return "time"
default:
return "unknown type"
}

View File

@@ -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)