Skip to content

Commit 0223236

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 ac104c8 commit 0223236

File tree

6 files changed

+314
-127
lines changed

6 files changed

+314
-127
lines changed

Diff for: constants.go

+1
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const (
1010
annotationLinks = "links"
1111
annotationOmitEmpty = "omitempty"
1212
annotationISO8601 = "iso8601"
13+
annotationRFC3339 = "rfc3339"
1314
annotationSeperator = ","
1415

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

Diff for: 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 {

Diff for: 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)
@@ -544,26 +547,25 @@ func handleLinks(attribute interface{}, args []string, fieldValue reflect.Value)
544547
}
545548

546549
func handleTime(attribute interface{}, args []string, fieldValue reflect.Value) (reflect.Value, error) {
547-
var isIso8601 bool
550+
var isISO8601, isRFC3339 bool
548551
v := reflect.ValueOf(attribute)
549552

550553
if len(args) > 2 {
551554
for _, arg := range args[2:] {
552555
if arg == annotationISO8601 {
553-
isIso8601 = true
556+
isISO8601 = true
557+
} else if arg == annotationRFC3339 {
558+
isRFC3339 = true
554559
}
555560
}
556561
}
557562

558-
if isIso8601 {
559-
var tm string
560-
if v.Kind() == reflect.String {
561-
tm = v.Interface().(string)
562-
} else {
563+
if isISO8601 {
564+
if v.Kind() != reflect.String {
563565
return reflect.ValueOf(time.Now()), ErrInvalidISO8601
564566
}
565567

566-
t, err := time.Parse(iso8601TimeFormat, tm)
568+
t, err := time.Parse(iso8601TimeFormat, v.Interface().(string))
567569
if err != nil {
568570
return reflect.ValueOf(time.Now()), ErrInvalidISO8601
569571
}
@@ -575,6 +577,23 @@ func handleTime(attribute interface{}, args []string, fieldValue reflect.Value)
575577
return reflect.ValueOf(t), nil
576578
}
577579

580+
if isRFC3339 {
581+
if v.Kind() != reflect.String {
582+
return reflect.ValueOf(time.Now()), ErrInvalidRFC3339
583+
}
584+
585+
t, err := time.Parse(time.RFC3339, v.Interface().(string))
586+
if err != nil {
587+
return reflect.ValueOf(time.Now()), ErrInvalidRFC3339
588+
}
589+
590+
if fieldValue.Kind() == reflect.Ptr {
591+
return reflect.ValueOf(&t), nil
592+
}
593+
594+
return reflect.ValueOf(t), nil
595+
}
596+
578597
var at int64
579598

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

Diff for: 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

Diff for: 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)