Skip to content

Commit 9246c91

Browse files
authored
Merge pull request #159 from shwoodard/shwoodard-defined-type-attributes
Add support for attributes of custom defined types
2 parents d9a6107 + e22856d commit 9246c91

File tree

5 files changed

+142
-37
lines changed

5 files changed

+142
-37
lines changed

README.md

+9-23
Original file line numberDiff line numberDiff line change
@@ -345,33 +345,19 @@ func (post Post) JSONAPIRelationshipMeta(relation string) *Meta {
345345

346346
### Custom types
347347

348-
If you need to support custom types (e.g. for custom time formats), you'll need to implement the json.Marshaler and json.Unmarshaler interfaces on the type.
348+
Custom types are supported for primitive types, only, as attributes. Examples,
349349

350350
```go
351-
// MyTimeFormat is a custom format I invented for fun
352-
const MyTimeFormat = "The time is 15:04:05. The year is 2006, and it is day 2 of January."
353-
354-
// MyTime is a custom type used to handle the custom time format
355-
type MyTime struct {
356-
time.Time
357-
}
358-
359-
// UnmarshalJSON to implement the json.Unmarshaler interface
360-
func (m *MyTime) UnmarshalJSON(b []byte) error {
361-
t, err := time.Parse(MyTimeFormat, string(b))
362-
if err != nil {
363-
return err
364-
}
365-
366-
m.Time = t
351+
type CustomIntType int
352+
type CustomFloatType float64
353+
type CustomStringType string
354+
```
367355

368-
return nil
369-
}
356+
Types like following are not supported, but may be in the future:
370357

371-
// MarshalJSON to implement the json.Marshaler interface
372-
func (m *MyTime) MarshalJSON() ([]byte, error) {
373-
return json.Marshal(m.Time.Format(MyTimeFormat))
374-
}
358+
```go
359+
type CustomMapType map[string]interface{}
360+
type CustomSliceMapType []map[string]interface{}
375361
```
376362

377363
### Errors

models_test.go

+15
Original file line numberDiff line numberDiff line change
@@ -176,3 +176,18 @@ type Employee struct {
176176
Age int `jsonapi:"attr,age"`
177177
HiredAt *time.Time `jsonapi:"attr,hired-at,iso8601"`
178178
}
179+
180+
type CustomIntType int
181+
type CustomFloatType float64
182+
type CustomStringType string
183+
184+
type CustomAttributeTypes struct {
185+
ID string `jsonapi:"primary,customtypes"`
186+
187+
Int CustomIntType `jsonapi:"attr,int"`
188+
IntPtr *CustomIntType `jsonapi:"attr,intptr"`
189+
IntPtrNull *CustomIntType `jsonapi:"attr,intptrnull"`
190+
191+
Float CustomFloatType `jsonapi:"attr,float"`
192+
String CustomStringType `jsonapi:"attr,string"`
193+
}

request.go

+29-5
Original file line numberDiff line numberDiff line change
@@ -254,8 +254,6 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
254254
}
255255

256256
assign(fieldValue, value)
257-
continue
258-
259257
} else if annotation == annotationRelation {
260258
isSlice := fieldValue.Type().Kind() == reflect.Slice
261259

@@ -347,10 +345,37 @@ func fullNode(n *Node, included *map[string]*Node) *Node {
347345
// assign will take the value specified and assign it to the field; if
348346
// field is expecting a ptr assign will assign a ptr.
349347
func assign(field, value reflect.Value) {
348+
value = reflect.Indirect(value)
349+
350350
if field.Kind() == reflect.Ptr {
351+
// initialize pointer so it's value
352+
// can be set by assignValue
353+
field.Set(reflect.New(field.Type().Elem()))
354+
field = field.Elem()
355+
356+
}
357+
358+
assignValue(field, value)
359+
}
360+
361+
// assign assigns the specified value to the field,
362+
// expecting both values not to be pointer types.
363+
func assignValue(field, value reflect.Value) {
364+
switch field.Kind() {
365+
case reflect.Int, reflect.Int8, reflect.Int16,
366+
reflect.Int32, reflect.Int64:
367+
field.SetInt(value.Int())
368+
case reflect.Uint, reflect.Uint8, reflect.Uint16,
369+
reflect.Uint32, reflect.Uint64, reflect.Uintptr:
370+
field.SetUint(value.Uint())
371+
case reflect.Float32, reflect.Float64:
372+
field.SetFloat(value.Float())
373+
case reflect.String:
374+
field.SetString(value.String())
375+
case reflect.Bool:
376+
field.SetBool(value.Bool())
377+
default:
351378
field.Set(value)
352-
} else {
353-
field.Set(reflect.Indirect(value))
354379
}
355380
}
356381

@@ -588,7 +613,6 @@ func handleStruct(
588613
return reflect.Value{}, err
589614
}
590615

591-
592616
return model, nil
593617
}
594618

request_test.go

+88-7
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,10 @@ func TestUnmarshalSetsID(t *testing.T) {
301301
func TestUnmarshal_nonNumericID(t *testing.T) {
302302
data := samplePayloadWithoutIncluded()
303303
data["data"].(map[string]interface{})["id"] = "non-numeric-id"
304-
payload, _ := payload(data)
304+
payload, err := json.Marshal(data)
305+
if err != nil {
306+
t.Fatal(err)
307+
}
305308
in := bytes.NewReader(payload)
306309
out := new(Post)
307310

@@ -402,7 +405,10 @@ func TestUnmarshalInvalidISO8601(t *testing.T) {
402405
}
403406

404407
func TestUnmarshalRelationshipsWithoutIncluded(t *testing.T) {
405-
data, _ := payload(samplePayloadWithoutIncluded())
408+
data, err := json.Marshal(samplePayloadWithoutIncluded())
409+
if err != nil {
410+
t.Fatal(err)
411+
}
406412
in := bytes.NewReader(data)
407413
out := new(Post)
408414

@@ -768,6 +774,86 @@ func TestManyPayload_withLinks(t *testing.T) {
768774
}
769775
}
770776

777+
func TestUnmarshalCustomTypeAttributes(t *testing.T) {
778+
customInt := CustomIntType(5)
779+
customFloat := CustomFloatType(1.5)
780+
customString := CustomStringType("Test")
781+
782+
data := map[string]interface{}{
783+
"data": map[string]interface{}{
784+
"type": "customtypes",
785+
"id": "1",
786+
"attributes": map[string]interface{}{
787+
"int": 5,
788+
"intptr": 5,
789+
"intptrnull": nil,
790+
791+
"float": 1.5,
792+
"string": "Test",
793+
},
794+
},
795+
}
796+
payload, err := json.Marshal(data)
797+
if err != nil {
798+
t.Fatal(err)
799+
}
800+
801+
// Parse JSON API payload
802+
customAttributeTypes := new(CustomAttributeTypes)
803+
if err := UnmarshalPayload(bytes.NewReader(payload), customAttributeTypes); err != nil {
804+
t.Fatal(err)
805+
}
806+
807+
if expected, actual := customInt, customAttributeTypes.Int; expected != actual {
808+
t.Fatalf("Was expecting custom int to be `%d`, got `%d`", expected, actual)
809+
}
810+
if expected, actual := customInt, *customAttributeTypes.IntPtr; expected != actual {
811+
t.Fatalf("Was expecting custom int pointer to be `%d`, got `%d`", expected, actual)
812+
}
813+
if customAttributeTypes.IntPtrNull != nil {
814+
t.Fatalf("Was expecting custom int pointer to be <nil>, got `%d`", customAttributeTypes.IntPtrNull)
815+
}
816+
817+
if expected, actual := customFloat, customAttributeTypes.Float; expected != actual {
818+
t.Fatalf("Was expecting custom float to be `%f`, got `%f`", expected, actual)
819+
}
820+
if expected, actual := customString, customAttributeTypes.String; expected != actual {
821+
t.Fatalf("Was expecting custom string to be `%s`, got `%s`", expected, actual)
822+
}
823+
}
824+
825+
func TestUnmarshalCustomTypeAttributes_ErrInvalidType(t *testing.T) {
826+
data := map[string]interface{}{
827+
"data": map[string]interface{}{
828+
"type": "customtypes",
829+
"id": "1",
830+
"attributes": map[string]interface{}{
831+
"int": "bad",
832+
"intptr": 5,
833+
"intptrnull": nil,
834+
835+
"float": 1.5,
836+
"string": "Test",
837+
},
838+
},
839+
}
840+
payload, err := json.Marshal(data)
841+
if err != nil {
842+
t.Fatal(err)
843+
}
844+
845+
// Parse JSON API payload
846+
customAttributeTypes := new(CustomAttributeTypes)
847+
err = UnmarshalPayload(bytes.NewReader(payload), customAttributeTypes)
848+
if err == nil {
849+
t.Fatal("Expected an error unmarshalling the payload due to type mismatch, got none")
850+
}
851+
852+
if err != ErrInvalidType {
853+
t.Fatalf("Expected error to be %v, was %v", ErrInvalidType, err)
854+
}
855+
}
856+
771857
func samplePayloadWithoutIncluded() map[string]interface{} {
772858
return map[string]interface{}{
773859
"data": map[string]interface{}{
@@ -801,11 +887,6 @@ func samplePayloadWithoutIncluded() map[string]interface{} {
801887
}
802888
}
803889

804-
func payload(data map[string]interface{}) (result []byte, err error) {
805-
result, err = json.Marshal(data)
806-
return
807-
}
808-
809890
func samplePayload() io.Reader {
810891
payload := &OnePayload{
811892
Data: &Node{

response_test.go

+1-2
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,9 @@ func TestMarshalPayload(t *testing.T) {
3939

4040
func TestMarshalPayloadWithNulls(t *testing.T) {
4141

42-
books := []*Book{nil, {ID:101}, nil}
42+
books := []*Book{nil, {ID: 101}, nil}
4343
var jsonData map[string]interface{}
4444

45-
4645
out := bytes.NewBuffer(nil)
4746
if err := MarshalPayload(out, books); err != nil {
4847
t.Fatal(err)

0 commit comments

Comments
 (0)