forked from Mirrors/jsonapi
Add support for iso8601 struct tag argument (#47)
* Add support for iso8601 struct tag. Currently only unix timestamps are supported for serialising time.Time objects. The JSON.API specification doesn't make a specific requirement on time serialisation format, though it does make a recommendation for using ISO8601. This adds support for an additional struct tag `iso8601` which controls serialisation and deserialisation using the ISO8601 timestamp format. * Update doc.go with information regarding iso8601 and omitempty tag arguments.
This commit is contained in:
parent
a1fa2c84a0
commit
b6c6609ff2
7
doc.go
7
doc.go
|
@ -45,12 +45,17 @@ value arguments are comma separated. The first argument must be, "primary", and
|
|||
the second must be the name that should appear in the "type" field for all data
|
||||
objects that represent this type of model.
|
||||
|
||||
Value, attr: "attr,<key name in attributes hash>"
|
||||
Value, attr: "attr,<key name in attributes hash>[,<extra arguments>]"
|
||||
|
||||
These fields' values should end up in the "attribute" hash for a record. The first
|
||||
argument must be, "attr', and the second should be the name for the key to display in
|
||||
the the "attributes" hash for that record.
|
||||
|
||||
The following extra arguments are also supported:
|
||||
|
||||
"omitempty": excludes the fields value from the "attribute" hash.
|
||||
"iso8601": uses the ISO8601 timestamp format when serialising or deserialising the time.Time value.
|
||||
|
||||
Value, relation: "relation,<key name in relationships hash>"
|
||||
|
||||
Relations are struct fields that represent a one-to-one or one-to-many to other structs.
|
||||
|
|
55
request.go
55
request.go
|
@ -20,6 +20,9 @@ var (
|
|||
// ErrInvalidTime is returned when a struct has a time.Time type field, but
|
||||
// the JSON value was not a unix timestamp integer.
|
||||
ErrInvalidTime = errors.New("Only numbers can be parsed as dates, unix timestamps")
|
||||
// ErrInvalidISO8601 is returned when a struct has a time.Time type field and includes
|
||||
// "iso8601" in the tag spec, but the JSON value was not an ISO8601 timestamp string.
|
||||
ErrInvalidISO8601 = errors.New("Only strings can be parsed as dates, ISO8601 timestamps")
|
||||
// ErrUnknownFieldNumberType is returned when the JSON value was a float
|
||||
// (numeric) but the Struct field was a non numeric type (i.e. not int, uint,
|
||||
// float, etc)
|
||||
|
@ -193,6 +196,16 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
continue
|
||||
}
|
||||
|
||||
var iso8601 bool
|
||||
|
||||
if len(args) > 2 {
|
||||
for _, arg := range args[2:] {
|
||||
if arg == "iso8601" {
|
||||
iso8601 = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val := attributes[args[1]]
|
||||
|
||||
// continue if the attribute was not included in the request
|
||||
|
@ -204,6 +217,26 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
|
||||
// Handle field of type time.Time
|
||||
if fieldValue.Type() == reflect.TypeOf(time.Time{}) {
|
||||
if iso8601 {
|
||||
var tm string
|
||||
if v.Kind() == reflect.String {
|
||||
tm = v.Interface().(string)
|
||||
} else {
|
||||
er = ErrInvalidISO8601
|
||||
break
|
||||
}
|
||||
|
||||
t, err := time.Parse(iso8601TimeFormat, tm)
|
||||
if err != nil {
|
||||
er = ErrInvalidISO8601
|
||||
break
|
||||
}
|
||||
|
||||
fieldValue.Set(reflect.ValueOf(t))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var at int64
|
||||
|
||||
if v.Kind() == reflect.Float64 {
|
||||
|
@ -234,6 +267,28 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
|
|||
}
|
||||
|
||||
if fieldValue.Type() == reflect.TypeOf(new(time.Time)) {
|
||||
if iso8601 {
|
||||
var tm string
|
||||
if v.Kind() == reflect.String {
|
||||
tm = v.Interface().(string)
|
||||
} else {
|
||||
er = ErrInvalidISO8601
|
||||
break
|
||||
}
|
||||
|
||||
v, err := time.Parse(iso8601TimeFormat, tm)
|
||||
if err != nil {
|
||||
er = ErrInvalidISO8601
|
||||
break
|
||||
}
|
||||
|
||||
t := &v
|
||||
|
||||
fieldValue.Set(reflect.ValueOf(t))
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var at int64
|
||||
|
||||
if v.Kind() == reflect.Float64 {
|
||||
|
|
|
@ -148,6 +148,78 @@ func TestUnmarshalSetsAttrs(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalParsesISO8601(t *testing.T) {
|
||||
payload := &OnePayload{
|
||||
Data: &Node{
|
||||
Type: "timestamps",
|
||||
Attributes: map[string]interface{}{
|
||||
"timestamp": "2016-08-17T08:27:12Z",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
in := bytes.NewBuffer(nil)
|
||||
json.NewEncoder(in).Encode(payload)
|
||||
|
||||
out := new(Timestamp)
|
||||
|
||||
if err := UnmarshalPayload(in, out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC)
|
||||
|
||||
if !out.Time.Equal(expected) {
|
||||
t.Fatal("Parsing the ISO8601 timestamp failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalParsesISO8601TimePointer(t *testing.T) {
|
||||
payload := &OnePayload{
|
||||
Data: &Node{
|
||||
Type: "timestamps",
|
||||
Attributes: map[string]interface{}{
|
||||
"next": "2016-08-17T08:27:12Z",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
in := bytes.NewBuffer(nil)
|
||||
json.NewEncoder(in).Encode(payload)
|
||||
|
||||
out := new(Timestamp)
|
||||
|
||||
if err := UnmarshalPayload(in, out); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
expected := time.Date(2016, 8, 17, 8, 27, 12, 0, time.UTC)
|
||||
|
||||
if !out.Next.Equal(expected) {
|
||||
t.Fatal("Parsing the ISO8601 timestamp failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalInvalidISO8601(t *testing.T) {
|
||||
payload := &OnePayload{
|
||||
Data: &Node{
|
||||
Type: "timestamps",
|
||||
Attributes: map[string]interface{}{
|
||||
"timestamp": "17 Aug 16 08:027 MST",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
in := bytes.NewBuffer(nil)
|
||||
json.NewEncoder(in).Encode(payload)
|
||||
|
||||
out := new(Timestamp)
|
||||
|
||||
if err := UnmarshalPayload(in, out); err != ErrInvalidISO8601 {
|
||||
t.Fatalf("Expected ErrInvalidISO8601, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnmarshalRelationshipsWithoutIncluded(t *testing.T) {
|
||||
data, _ := samplePayloadWithoutIncluded()
|
||||
in := bytes.NewReader(data)
|
||||
|
|
21
response.go
21
response.go
|
@ -24,6 +24,8 @@ var (
|
|||
ErrExpectedSlice = errors.New("models should be a slice of struct pointers")
|
||||
)
|
||||
|
||||
const iso8601TimeFormat = "2006-01-02T15:04:05Z"
|
||||
|
||||
// MarshalOnePayload writes a jsonapi response with one, with related records
|
||||
// sideloaded, into "included" array. This method encodes a response for a
|
||||
// single record only. Hence, data will be a single record rather than an array
|
||||
|
@ -236,10 +238,17 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
|||
node.ClientID = clientID
|
||||
}
|
||||
} else if annotation == "attr" {
|
||||
var omitEmpty bool
|
||||
var omitEmpty, iso8601 bool
|
||||
|
||||
if len(args) > 2 {
|
||||
omitEmpty = args[2] == "omitempty"
|
||||
for _, arg := range args[2:] {
|
||||
switch arg {
|
||||
case "omitempty":
|
||||
omitEmpty = true
|
||||
case "iso8601":
|
||||
iso8601 = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if node.Attributes == nil {
|
||||
|
@ -253,7 +262,11 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
|||
continue
|
||||
}
|
||||
|
||||
if iso8601 {
|
||||
node.Attributes[args[1]] = t.UTC().Format(iso8601TimeFormat)
|
||||
} else {
|
||||
node.Attributes[args[1]] = t.Unix()
|
||||
}
|
||||
} else if fieldValue.Type() == reflect.TypeOf(new(time.Time)) {
|
||||
// A time pointer may be nil
|
||||
if fieldValue.IsNil() {
|
||||
|
@ -269,8 +282,12 @@ func visitModelNode(model interface{}, included *map[string]*Node, sideload bool
|
|||
continue
|
||||
}
|
||||
|
||||
if iso8601 {
|
||||
node.Attributes[args[1]] = tm.UTC().Format(iso8601TimeFormat)
|
||||
} else {
|
||||
node.Attributes[args[1]] = tm.Unix()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Dealing with a fieldValue that is not a time
|
||||
emptyValue := reflect.Zero(fieldValue.Type())
|
||||
|
|
|
@ -47,6 +47,12 @@ type Book struct {
|
|||
PublishedAt time.Time
|
||||
}
|
||||
|
||||
type Timestamp struct {
|
||||
ID int `jsonapi:"primary,timestamps"`
|
||||
Time time.Time `jsonapi:"attr,timestamp,iso8601"`
|
||||
Next *time.Time `jsonapi:"attr,next,iso8601"`
|
||||
}
|
||||
|
||||
func TestOmitsEmptyAnnotation(t *testing.T) {
|
||||
book := &Book{
|
||||
Author: "aren55555",
|
||||
|
@ -168,6 +174,61 @@ func TestOmitsZeroTimes(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestMarshalISO8601Time(t *testing.T) {
|
||||
testModel := &Timestamp{
|
||||
ID: 5,
|
||||
Time: time.Date(2016, 8, 17, 8, 27, 12, 23849, time.UTC),
|
||||
}
|
||||
|
||||
out := bytes.NewBuffer(nil)
|
||||
if err := MarshalOnePayload(out, testModel); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp := new(OnePayload)
|
||||
if err := json.NewDecoder(out).Decode(resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data := resp.Data
|
||||
|
||||
if data.Attributes == nil {
|
||||
t.Fatalf("Expected attributes")
|
||||
}
|
||||
|
||||
if data.Attributes["timestamp"] != "2016-08-17T08:27:12Z" {
|
||||
t.Fatal("Timestamp was not serialised into ISO8601 correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMarshalISO8601TimePointer(t *testing.T) {
|
||||
tm := time.Date(2016, 8, 17, 8, 27, 12, 23849, time.UTC)
|
||||
testModel := &Timestamp{
|
||||
ID: 5,
|
||||
Next: &tm,
|
||||
}
|
||||
|
||||
out := bytes.NewBuffer(nil)
|
||||
if err := MarshalOnePayload(out, testModel); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
resp := new(OnePayload)
|
||||
if err := json.NewDecoder(out).Decode(resp); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
data := resp.Data
|
||||
|
||||
if data.Attributes == nil {
|
||||
t.Fatalf("Expected attributes")
|
||||
}
|
||||
|
||||
if data.Attributes["next"] != "2016-08-17T08:27:12Z" {
|
||||
t.Fatal("Next was not serialised into ISO8601 correctly")
|
||||
}
|
||||
}
|
||||
|
||||
func TestRelations(t *testing.T) {
|
||||
testModel := testBlog()
|
||||
|
||||
|
|
Loading…
Reference in New Issue