Skip to content

Commit be710af

Browse files
omarismailctrombley
authored andcommitted
Add RFC3339 timestamp (google#201)
@omarismail LGTM. I'm curious what you think about perhaps documenting these `iso8601` and `rfc3339` in the `Readme.md`? How did you find that this tag option/value existed? How can we make this better for others vs having to search the library implementation?
1 parent e32c6a1 commit be710af

File tree

4 files changed

+106
-0
lines changed

4 files changed

+106
-0
lines changed

Diff for: constants.go

+2
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,10 @@ const (
99
annotationRelation = "relation"
1010
annotationOmitEmpty = "omitempty"
1111
annotationISO8601 = "iso8601"
12+
annotationRFC3339 = "rfc3339"
1213
annotationSeperator = ","
1314

15+
rfc3339TimeFormat = "2006-01-02T15:04:05Z07:00"
1416
iso8601TimeFormat = "2006-01-02T15:04:05Z"
1517

1618
// MediaType is the identifier for the JSON API media type

Diff for: models_test.go

+6
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ type Timestamp struct {
3131
Next *time.Time `jsonapi:"attr,next,iso8601"`
3232
}
3333

34+
type TimestampRFC3339 struct {
35+
ID int `jsonapi:"primary,timestamps"`
36+
Time time.Time `jsonapi:"attr,timestamp,rfc3339"`
37+
Next *time.Time `jsonapi:"attr,next,rfc3339"`
38+
}
39+
3440
type Car struct {
3541
ID *string `jsonapi:"primary,cars"`
3642
Make *string `jsonapi:"attr,make,omitempty"`

Diff for: request.go

+26
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@ var (
2323
// ErrInvalidISO8601 is returned when a struct has a time.Time type field and includes
2424
// "iso8601" in the tag spec, but the JSON value was not an ISO8601 timestamp string.
2525
ErrInvalidISO8601 = errors.New("Only strings can be parsed as dates, ISO8601 timestamps")
26+
// ErrInvalidRFC3339 is returned when a struct has a time.Time type field and includes
27+
// "rfc3339" in the tag spec, but the JSON value was not an RFC3339 timestamp string.
28+
ErrInvalidRFC3339 = errors.New("Only strings can be parsed as dates, RFC3339 timestamps")
2629
// ErrUnknownFieldNumberType is returned when the JSON value was a float
2730
// (numeric) but the Struct field was a non numeric type (i.e. not int, uint,
2831
// float, etc)
@@ -446,12 +449,15 @@ func handleStringSlice(attribute interface{}) (reflect.Value, error) {
446449

447450
func handleTime(attribute interface{}, args []string, fieldValue reflect.Value) (reflect.Value, error) {
448451
var isIso8601 bool
452+
var isRFC3339 bool
449453
v := reflect.ValueOf(attribute)
450454

451455
if len(args) > 2 {
452456
for _, arg := range args[2:] {
453457
if arg == annotationISO8601 {
454458
isIso8601 = true
459+
} else if arg == annotationRFC3339 {
460+
isRFC3339 = true
455461
}
456462
}
457463
}
@@ -476,6 +482,26 @@ func handleTime(attribute interface{}, args []string, fieldValue reflect.Value)
476482
return reflect.ValueOf(t), nil
477483
}
478484

485+
if isRFC3339 {
486+
var tm string
487+
if v.Kind() == reflect.String {
488+
tm = v.Interface().(string)
489+
} else {
490+
return reflect.ValueOf(time.Now()), ErrInvalidRFC3339
491+
}
492+
493+
t, err := time.Parse(time.RFC3339, tm)
494+
if err != nil {
495+
return reflect.ValueOf(time.Now()), ErrInvalidRFC3339
496+
}
497+
498+
if fieldValue.Kind() == reflect.Ptr {
499+
return reflect.ValueOf(&t), nil
500+
}
501+
502+
return reflect.ValueOf(t), nil
503+
}
504+
479505
var at int64
480506

481507
if v.Kind() == reflect.Float64 {

Diff for: request_test.go

+72
Original file line numberDiff line numberDiff line change
@@ -413,6 +413,78 @@ func TestUnmarshalInvalidISO8601(t *testing.T) {
413413
}
414414
}
415415

416+
func TestUnmarshalParsesRFC3339(t *testing.T) {
417+
payload := &OnePayload{
418+
Data: &Node{
419+
Type: "timestamps",
420+
Attributes: map[string]interface{}{
421+
"timestamp": "2020-03-16T23:09:59+00:00",
422+
},
423+
},
424+
}
425+
426+
in := bytes.NewBuffer(nil)
427+
json.NewEncoder(in).Encode(payload)
428+
429+
out := new(TimestampRFC3339)
430+
431+
if err := UnmarshalPayload(in, out); err != nil {
432+
t.Fatal(err)
433+
}
434+
435+
expected := time.Date(2020, 3, 16, 23, 9, 59, 0, time.UTC)
436+
437+
if !out.Time.Equal(expected) {
438+
t.Fatal("Parsing the RFC3339 timestamp failed")
439+
}
440+
}
441+
442+
func TestUnmarshalParsesRFC3339TimePointer(t *testing.T) {
443+
payload := &OnePayload{
444+
Data: &Node{
445+
Type: "timestamps",
446+
Attributes: map[string]interface{}{
447+
"next": "2020-03-16T23:09:59+00:00",
448+
},
449+
},
450+
}
451+
452+
in := bytes.NewBuffer(nil)
453+
json.NewEncoder(in).Encode(payload)
454+
455+
out := new(TimestampRFC3339)
456+
457+
if err := UnmarshalPayload(in, out); err != nil {
458+
t.Fatal(err)
459+
}
460+
461+
expected := time.Date(2020, 3, 16, 23, 9, 59, 0, time.UTC)
462+
463+
if !out.Next.Equal(expected) {
464+
t.Fatal("Parsing the RFC3339 timestamp failed")
465+
}
466+
}
467+
468+
func TestUnmarshalInvalidRFC3339(t *testing.T) {
469+
payload := &OnePayload{
470+
Data: &Node{
471+
Type: "timestamps",
472+
Attributes: map[string]interface{}{
473+
"timestamp": "17 Aug 16 08:027 MST",
474+
},
475+
},
476+
}
477+
478+
in := bytes.NewBuffer(nil)
479+
json.NewEncoder(in).Encode(payload)
480+
481+
out := new(TimestampRFC3339)
482+
483+
if err := UnmarshalPayload(in, out); err != ErrInvalidRFC3339 {
484+
t.Fatalf("Expected ErrInvalidRFC3339, got %v", err)
485+
}
486+
}
487+
416488
func TestUnmarshalRelationshipsWithoutIncluded(t *testing.T) {
417489
data, err := json.Marshal(samplePayloadWithoutIncluded())
418490
if err != nil {

0 commit comments

Comments
 (0)