Skip to content

Commit 9db1fe8

Browse files
aren55555quetzyg
authored andcommitted
RFC 3339 support for both Marshal and Unmarshal. (google#204)
* RFC 3339 support for both Marshal and Unmarshal. * Post merge cleanup * Update request_test.go Co-authored-by: Quetzy Garcia <[email protected]> * Spelling * Update request.go Co-authored-by: Quetzy Garcia <[email protected]> * Simplify the ISO 8601 logic. No need for the const rfc3339TimeFormat use time.RFC3339 directly. Co-authored-by: Quetzy Garcia <[email protected]>
1 parent bb1e378 commit 9db1fe8

File tree

6 files changed

+314
-127
lines changed

6 files changed

+314
-127
lines changed

constants.go

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

1415
iso8601TimeFormat = "2006-01-02T15:04:05Z"

models_test.go

+8-4
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,14 @@ type WithPointer struct {
2525
FloatVal *float32 `jsonapi:"attr,float-val"`
2626
}
2727

28-
type Timestamp struct {
29-
ID int `jsonapi:"primary,timestamps"`
30-
Time time.Time `jsonapi:"attr,timestamp,iso8601"`
31-
Next *time.Time `jsonapi:"attr,next,iso8601"`
28+
type TimestampModel struct {
29+
ID int `jsonapi:"primary,timestamps"`
30+
DefaultV time.Time `jsonapi:"attr,defaultv"`
31+
DefaultP *time.Time `jsonapi:"attr,defaultp"`
32+
ISO8601V time.Time `jsonapi:"attr,iso8601v,iso8601"`
33+
ISO8601P *time.Time `jsonapi:"attr,iso8601p,iso8601"`
34+
RFC3339V time.Time `jsonapi:"attr,rfc3339v,rfc3339"`
35+
RFC3339P *time.Time `jsonapi:"attr,rfc3339p,rfc3339"`
3236
}
3337

3438
type Car struct {

request.go

+27-8
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)
@@ -445,26 +448,25 @@ func handleStringSlice(attribute interface{}) (reflect.Value, error) {
445448
}
446449

447450
func handleTime(attribute interface{}, args []string, fieldValue reflect.Value) (reflect.Value, error) {
448-
var isIso8601 bool
451+
var isISO8601, isRFC3339 bool
449452
v := reflect.ValueOf(attribute)
450453

451454
if len(args) > 2 {
452455
for _, arg := range args[2:] {
453456
if arg == annotationISO8601 {
454-
isIso8601 = true
457+
isISO8601 = true
458+
} else if arg == annotationRFC3339 {
459+
isRFC3339 = true
455460
}
456461
}
457462
}
458463

459-
if isIso8601 {
460-
var tm string
461-
if v.Kind() == reflect.String {
462-
tm = v.Interface().(string)
463-
} else {
464+
if isISO8601 {
465+
if v.Kind() != reflect.String {
464466
return reflect.ValueOf(time.Now()), ErrInvalidISO8601
465467
}
466468

467-
t, err := time.Parse(iso8601TimeFormat, tm)
469+
t, err := time.Parse(iso8601TimeFormat, v.Interface().(string))
468470
if err != nil {
469471
return reflect.ValueOf(time.Now()), ErrInvalidISO8601
470472
}
@@ -476,6 +478,23 @@ func handleTime(attribute interface{}, args []string, fieldValue reflect.Value)
476478
return reflect.ValueOf(t), nil
477479
}
478480

481+
if isRFC3339 {
482+
if v.Kind() != reflect.String {
483+
return reflect.ValueOf(time.Now()), ErrInvalidRFC3339
484+
}
485+
486+
t, err := time.Parse(time.RFC3339, v.Interface().(string))
487+
if err != nil {
488+
return reflect.ValueOf(time.Now()), ErrInvalidRFC3339
489+
}
490+
491+
if fieldValue.Kind() == reflect.Ptr {
492+
return reflect.ValueOf(&t), nil
493+
}
494+
495+
return reflect.ValueOf(t), nil
496+
}
497+
479498
var at int64
480499

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

request_test.go

+163-62
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package jsonapi
33
import (
44
"bytes"
55
"encoding/json"
6+
"errors"
67
"fmt"
78
"io"
89
"reflect"
@@ -341,75 +342,175 @@ func TestUnmarshalSetsAttrs(t *testing.T) {
341342
}
342343
}
343344

344-
func TestUnmarshalParsesISO8601(t *testing.T) {
345-
payload := &OnePayload{
346-
Data: &Node{
347-
Type: "timestamps",
348-
Attributes: map[string]interface{}{
349-
"timestamp": "2016-08-17T08:27:12Z",
345+
func TestUnmarshal_Times(t *testing.T) {
346+
aTime := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC)
347+
348+
for _, tc := range []struct {
349+
desc string
350+
inputPayload *OnePayload
351+
wantErr bool
352+
verification func(tm *TimestampModel) error
353+
}{
354+
// Default:
355+
{
356+
desc: "default_byValue",
357+
inputPayload: &OnePayload{
358+
Data: &Node{
359+
Type: "timestamps",
360+
Attributes: map[string]interface{}{
361+
"defaultv": aTime.Unix(),
362+
},
363+
},
364+
},
365+
verification: func(tm *TimestampModel) error {
366+
if !tm.DefaultV.Equal(aTime) {
367+
return errors.New("times not equal!")
368+
}
369+
return nil
350370
},
351371
},
352-
}
353-
354-
in := bytes.NewBuffer(nil)
355-
json.NewEncoder(in).Encode(payload)
356-
357-
out := new(Timestamp)
358-
359-
if err := UnmarshalPayload(in, out); err != nil {
360-
t.Fatal(err)
361-
}
362-
363-
expected := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC)
364-
365-
if !out.Time.Equal(expected) {
366-
t.Fatal("Parsing the ISO8601 timestamp failed")
367-
}
368-
}
369-
370-
func TestUnmarshalParsesISO8601TimePointer(t *testing.T) {
371-
payload := &OnePayload{
372-
Data: &Node{
373-
Type: "timestamps",
374-
Attributes: map[string]interface{}{
375-
"next": "2016-08-17T08:27:12Z",
372+
{
373+
desc: "default_byPointer",
374+
inputPayload: &OnePayload{
375+
Data: &Node{
376+
Type: "timestamps",
377+
Attributes: map[string]interface{}{
378+
"defaultp": aTime.Unix(),
379+
},
380+
},
381+
},
382+
verification: func(tm *TimestampModel) error {
383+
if !tm.DefaultP.Equal(aTime) {
384+
return errors.New("times not equal!")
385+
}
386+
return nil
376387
},
377388
},
378-
}
379-
380-
in := bytes.NewBuffer(nil)
381-
json.NewEncoder(in).Encode(payload)
382-
383-
out := new(Timestamp)
384-
385-
if err := UnmarshalPayload(in, out); err != nil {
386-
t.Fatal(err)
387-
}
388-
389-
expected := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC)
390-
391-
if !out.Next.Equal(expected) {
392-
t.Fatal("Parsing the ISO8601 timestamp failed")
393-
}
394-
}
395-
396-
func TestUnmarshalInvalidISO8601(t *testing.T) {
397-
payload := &OnePayload{
398-
Data: &Node{
399-
Type: "timestamps",
400-
Attributes: map[string]interface{}{
401-
"timestamp": "17 Aug 16 08:027 MST",
389+
{
390+
desc: "default_invalid",
391+
inputPayload: &OnePayload{
392+
Data: &Node{
393+
Type: "timestamps",
394+
Attributes: map[string]interface{}{
395+
"defaultv": "not a timestamp!",
396+
},
397+
},
402398
},
399+
wantErr: true,
403400
},
404-
}
405-
406-
in := bytes.NewBuffer(nil)
407-
json.NewEncoder(in).Encode(payload)
408-
409-
out := new(Timestamp)
401+
// ISO 8601:
402+
{
403+
desc: "iso8601_byValue",
404+
inputPayload: &OnePayload{
405+
Data: &Node{
406+
Type: "timestamps",
407+
Attributes: map[string]interface{}{
408+
"iso8601v": "2016-08-17T08:27:12Z",
409+
},
410+
},
411+
},
412+
verification: func(tm *TimestampModel) error {
413+
if !tm.ISO8601V.Equal(aTime) {
414+
return errors.New("times not equal!")
415+
}
416+
return nil
417+
},
418+
},
419+
{
420+
desc: "iso8601_byPointer",
421+
inputPayload: &OnePayload{
422+
Data: &Node{
423+
Type: "timestamps",
424+
Attributes: map[string]interface{}{
425+
"iso8601p": "2016-08-17T08:27:12Z",
426+
},
427+
},
428+
},
429+
verification: func(tm *TimestampModel) error {
430+
if !tm.ISO8601P.Equal(aTime) {
431+
return errors.New("times not equal!")
432+
}
433+
return nil
434+
},
435+
},
436+
{
437+
desc: "iso8601_invalid",
438+
inputPayload: &OnePayload{
439+
Data: &Node{
440+
Type: "timestamps",
441+
Attributes: map[string]interface{}{
442+
"iso8601v": "not a timestamp",
443+
},
444+
},
445+
},
446+
wantErr: true,
447+
},
448+
// RFC 3339
449+
{
450+
desc: "rfc3339_byValue",
451+
inputPayload: &OnePayload{
452+
Data: &Node{
453+
Type: "timestamps",
454+
Attributes: map[string]interface{}{
455+
"rfc3339v": "2016-08-17T08:27:12Z",
456+
},
457+
},
458+
},
459+
verification: func(tm *TimestampModel) error {
460+
if got, want := tm.RFC3339V, aTime; got != want {
461+
return fmt.Errorf("got %v, want %v", got, want)
462+
}
463+
return nil
464+
},
465+
},
466+
{
467+
desc: "rfc3339_byPointer",
468+
inputPayload: &OnePayload{
469+
Data: &Node{
470+
Type: "timestamps",
471+
Attributes: map[string]interface{}{
472+
"rfc3339p": "2016-08-17T08:27:12Z",
473+
},
474+
},
475+
},
476+
verification: func(tm *TimestampModel) error {
477+
if got, want := *tm.RFC3339P, aTime; got != want {
478+
return fmt.Errorf("got %v, want %v", got, want)
479+
}
480+
return nil
481+
},
482+
},
483+
{
484+
desc: "rfc3339_invalid",
485+
inputPayload: &OnePayload{
486+
Data: &Node{
487+
Type: "timestamps",
488+
Attributes: map[string]interface{}{
489+
"rfc3339v": "not a timestamp",
490+
},
491+
},
492+
},
493+
wantErr: true,
494+
},
495+
} {
496+
t.Run(tc.desc, func(t *testing.T) {
497+
// Serialize the OnePayload using the standard JSON library.
498+
in := bytes.NewBuffer(nil)
499+
if err := json.NewEncoder(in).Encode(tc.inputPayload); err != nil {
500+
t.Fatal(err)
501+
}
410502

411-
if err := UnmarshalPayload(in, out); err != ErrInvalidISO8601 {
412-
t.Fatalf("Expected ErrInvalidISO8601, got %v", err)
503+
out := &TimestampModel{}
504+
err := UnmarshalPayload(in, out)
505+
if got, want := (err != nil), tc.wantErr; got != want {
506+
t.Fatalf("UnmarshalPayload error: got %v, want %v", got, want)
507+
}
508+
if tc.verification != nil {
509+
if err := tc.verification(out); err != nil {
510+
t.Fatal(err)
511+
}
512+
}
513+
})
413514
}
414515
}
415516

response.go

+7-1
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,7 @@ func visitModelNode(model interface{}, included *map[string]*Node,
283283
node.ClientID = clientID
284284
}
285285
} else if annotation == annotationAttribute {
286-
var omitEmpty, iso8601 bool
286+
var omitEmpty, iso8601, rfc3339 bool
287287

288288
if len(args) > 2 {
289289
for _, arg := range args[2:] {
@@ -292,6 +292,8 @@ func visitModelNode(model interface{}, included *map[string]*Node,
292292
omitEmpty = true
293293
case annotationISO8601:
294294
iso8601 = true
295+
case annotationRFC3339:
296+
rfc3339 = true
295297
}
296298
}
297299
}
@@ -309,6 +311,8 @@ func visitModelNode(model interface{}, included *map[string]*Node,
309311

310312
if iso8601 {
311313
node.Attributes[args[1]] = t.UTC().Format(iso8601TimeFormat)
314+
} else if rfc3339 {
315+
node.Attributes[args[1]] = t.UTC().Format(time.RFC3339)
312316
} else {
313317
node.Attributes[args[1]] = t.Unix()
314318
}
@@ -329,6 +333,8 @@ func visitModelNode(model interface{}, included *map[string]*Node,
329333

330334
if iso8601 {
331335
node.Attributes[args[1]] = tm.UTC().Format(iso8601TimeFormat)
336+
} else if rfc3339 {
337+
node.Attributes[args[1]] = tm.UTC().Format(time.RFC3339)
332338
} else {
333339
node.Attributes[args[1]] = tm.Unix()
334340
}

0 commit comments

Comments
 (0)