Skip to content

Commit 91ece5c

Browse files
committed
Support custom types (#114)
This applies to: - ID - Attributes - Relationships Custom types need to be registered before usage, using the new `jsonapi.RegisterType()` function which takes 3 arguments: - the type to support (`reflect.Type`) - the function to use when marshalling a response - the function to use when unmarshalling a response Example: ```` RegisterType(uuidType, func(value interface{}) (string, error) { result := value.(*UUID).String() return result, nil }, func(value string) (interface{}, error) { return UUIDFromString(value) }) ```` The custom type will be represented as a `string` in the JSON document in the requests and responses. Fixes #114 Signed-off-by: Xavier Coulon <[email protected]>
1 parent 3e6ead0 commit 91ece5c

7 files changed

+424
-37
lines changed

custom_types.go

+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package jsonapi
2+
3+
import "reflect"
4+
5+
type MarshallingFunc func(interface{}) (string, error)
6+
type UnmarshallingFunc func(string) (interface{}, error)
7+
8+
// map of functions to use to convert the field value into a JSON string
9+
var customTypeMarshallingFuncs map[reflect.Type]MarshallingFunc
10+
11+
// map of functions to use to convert the JSON string value into the target field
12+
var customTypeUnmarshallingFuncs map[reflect.Type]UnmarshallingFunc
13+
14+
// init initializes the maps
15+
func init() {
16+
customTypeMarshallingFuncs = make(map[reflect.Type]MarshallingFunc, 0)
17+
customTypeUnmarshallingFuncs = make(map[reflect.Type]UnmarshallingFunc, 0)
18+
}
19+
20+
// IsRegisteredType checks if the given type `t` is registered as a custom type
21+
func IsRegisteredType(t reflect.Type) bool {
22+
_, ok := customTypeMarshallingFuncs[t]
23+
return ok
24+
}
25+
26+
// RegisterType registers the functions to convert the field from a custom type to a string and vice-versa
27+
// in the JSON requests/responses.
28+
// The `marshallingFunc` must be a function that returns a string (along with an error if something wrong happened)
29+
// and the `unmarshallingFunc` must be a function that takes
30+
// a string as its sole argument and return an instance of `typeName` (along with an error if something wrong happened).
31+
// Eg: `uuid.FromString(string) uuid.UUID {...} and `uuid.String() string {...}
32+
func RegisterType(customType reflect.Type, marshallingFunc MarshallingFunc, unmarshallingFunc UnmarshallingFunc) {
33+
// register the pointer to the type
34+
customTypeMarshallingFuncs[customType] = marshallingFunc
35+
customTypeUnmarshallingFuncs[customType] = unmarshallingFunc
36+
}
37+
38+
// resetCustomTypeRegistrations resets the custom type registration, which is useful during testing
39+
func resetCustomTypeRegistrations() {
40+
customTypeMarshallingFuncs = make(map[reflect.Type]MarshallingFunc, 0)
41+
customTypeUnmarshallingFuncs = make(map[reflect.Type]UnmarshallingFunc, 0)
42+
}

custom_types_test.go

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package jsonapi
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func TestRegisterCustomTypes(t *testing.T) {
9+
for _, uuidType := range []reflect.Type{reflect.TypeOf(UUID{}), reflect.TypeOf(&UUID{})} {
10+
// given
11+
resetCustomTypeRegistrations() // make sure no other registration interferes with this test
12+
// when
13+
RegisterType(uuidType,
14+
func(value interface{}) (string, error) {
15+
return "", nil
16+
},
17+
func(value string) (interface{}, error) {
18+
return nil, nil
19+
})
20+
// then
21+
if !IsRegisteredType(uuidType) {
22+
t.Fatalf("Expected `%v` to be registered but it was not", uuidType)
23+
}
24+
}
25+
}

models_test.go

+38
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,44 @@ type ModelBadTypes struct {
1717
TimePtrField *time.Time `jsonapi:"attr,time_ptr_field"`
1818
}
1919

20+
type ModelWithUUIDs struct {
21+
ID UUID `jsonapi:"primary,customtypes"`
22+
UUIDField UUID `jsonapi:"attr,uuid_field"`
23+
LatestRelatedModel *RelatedModelWithUUIDs `jsonapi:"relation,latest_relatedmodel"`
24+
RelatedModels []*RelatedModelWithUUIDs `jsonapi:"relation,relatedmodels"`
25+
}
26+
type RelatedModelWithUUIDs struct {
27+
ID UUID `jsonapi:"primary,relatedtypes"`
28+
UUIDField UUID `jsonapi:"attr,uuid_field"`
29+
}
30+
31+
type ModelWithUUIDPtrs struct {
32+
ID *UUID `jsonapi:"primary,customtypes"`
33+
UUIDField *UUID `jsonapi:"attr,uuid_field"`
34+
LatestRelatedModel *RelatedModelWithUUIDPtrs `jsonapi:"relation,latest_relatedmodel"`
35+
RelatedModels []*RelatedModelWithUUIDPtrs `jsonapi:"relation,relatedmodels"`
36+
}
37+
38+
type RelatedModelWithUUIDPtrs struct {
39+
ID *UUID `jsonapi:"primary,relatedtypes"`
40+
UUIDField *UUID `jsonapi:"attr,uuid_field"`
41+
}
42+
43+
type UUID struct {
44+
string
45+
}
46+
47+
func UUIDFromString(s string) (*UUID, error) {
48+
return &UUID{s}, nil
49+
}
50+
func (u UUID) String() string {
51+
return u.string
52+
}
53+
54+
func (u UUID) Equal(other UUID) bool {
55+
return u.string == other.string
56+
}
57+
2058
type WithPointer struct {
2159
ID *uint64 `jsonapi:"primary,with-pointers"`
2260
Name *string `jsonapi:"attr,name"`

request.go

+25-2
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
120120
func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) {
121121
defer func() {
122122
if r := recover(); r != nil {
123-
err = fmt.Errorf("data is not a jsonapi representation of '%v'", model.Type())
123+
err = fmt.Errorf("data is not a jsonapi representation of '%v': %v", model.Type(), r)
124124
}
125125
}()
126126

@@ -168,7 +168,7 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
168168
break
169169
}
170170

171-
// ID will have to be transmitted as astring per the JSON API spec
171+
// ID will have to be transmitted as a string per the JSON API spec
172172
v := reflect.ValueOf(data.ID)
173173

174174
// Deal with PTRS
@@ -184,6 +184,17 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
184184
assign(fieldValue, v)
185185
continue
186186
}
187+
// custom type that can be unmarshalled from a string
188+
if v.Kind() == reflect.String && IsRegisteredType(fieldType.Type) {
189+
unmashalFunc := customTypeUnmarshallingFuncs[fieldType.Type]
190+
r, err := unmashalFunc(data.ID)
191+
if err != nil {
192+
er = err
193+
} else {
194+
fieldValue.Set(reflect.ValueOf(r))
195+
}
196+
continue
197+
}
187198

188199
// Value was not a string... only other supported type was a numeric,
189200
// which would have been sent as a float value.
@@ -418,6 +429,18 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
418429
continue
419430
}
420431

432+
// custom type that can be unmarshalled from a string
433+
if v.Kind() == reflect.String && IsRegisteredType(fieldType.Type) {
434+
unmashalFunc := customTypeUnmarshallingFuncs[fieldType.Type]
435+
r, err := unmashalFunc(val.(string))
436+
if err != nil {
437+
er = err
438+
} else {
439+
fieldValue.Set(reflect.ValueOf(r))
440+
}
441+
continue
442+
}
443+
421444
// Field was a Pointer type
422445
if fieldValue.Kind() == reflect.Ptr {
423446
var concreteVal reflect.Value

request_test.go

+148-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import (
1212
"time"
1313
)
1414

15-
func TestUnmarshall_attrStringSlice(t *testing.T) {
15+
func TestUnmarshal_attrStringSlice(t *testing.T) {
1616
out := &Book{}
1717
tags := []string{"fiction", "sale"}
1818
data := map[string]interface{}{
@@ -249,6 +249,89 @@ func TestUnmarshal_nonNumericID(t *testing.T) {
249249
}
250250
}
251251

252+
func TestUnmarshal_CustomType(t *testing.T) {
253+
// given
254+
// register the custom `UUID` type
255+
uuidType := reflect.TypeOf(UUID{})
256+
RegisterType(uuidType,
257+
func(value interface{}) (string, error) {
258+
return value.(UUID).String(), nil
259+
},
260+
func(value string) (interface{}, error) {
261+
result, err := UUIDFromString(value)
262+
if err != nil {
263+
fmt.Println("Error while converting from string to UUID: " + err.Error())
264+
return nil, err
265+
}
266+
return *result, nil
267+
})
268+
in := sampleModelCustomType()
269+
// when
270+
out := new(ModelWithUUIDs)
271+
if err := UnmarshalPayload(in, out); err != nil {
272+
t.Fatal(err)
273+
}
274+
// then
275+
if !out.ID.Equal(UUID{"12345678-abcd-1234-abcd-123456789012"}) {
276+
t.Fatalf("Did not set ID on dst interface: '%v'", out.ID)
277+
}
278+
if !out.UUIDField.Equal(UUID{"87654321-dcba-4321-dcba-210987654321"}) {
279+
t.Fatalf("Did not set UUIDField on dst interface: '%v'", out.UUIDField)
280+
}
281+
if !out.LatestRelatedModel.ID.Equal(UUID{"12345678-abcd-1234-abcd-111111111111"}) {
282+
t.Fatalf("Did not set LatestRelatedModel.ID on dst interface: '%v'", out.LatestRelatedModel.ID)
283+
}
284+
if !out.LatestRelatedModel.UUIDField.Equal(UUID{"87654321-dcba-4321-dcba-111111111111"}) {
285+
t.Fatalf("Did not set LatestRelatedModel.UUIDField on dst interface: '%v'", out.LatestRelatedModel.UUIDField)
286+
}
287+
if !out.RelatedModels[0].ID.Equal(UUID{"12345678-abcd-1234-abcd-222222222222"}) {
288+
t.Fatalf("Did not set RelatedModels[0].ID on dst interface: '%v'", out.LatestRelatedModel.ID)
289+
}
290+
if !out.RelatedModels[0].UUIDField.Equal(UUID{"87654321-dcba-4321-dcba-222222222222"}) {
291+
t.Fatalf("Did not set LatestRelatedModel.UUIDField on dst interface: '%v'", out.LatestRelatedModel.UUIDField)
292+
}
293+
294+
}
295+
296+
func TestUnmarshal_CustomType_Ptr(t *testing.T) {
297+
// given
298+
// register the custom `*UUID` type
299+
uuidType := reflect.TypeOf(&UUID{})
300+
RegisterType(uuidType,
301+
func(value interface{}) (string, error) {
302+
result := value.(*UUID).String()
303+
return result, nil
304+
},
305+
func(value string) (interface{}, error) {
306+
return UUIDFromString(value)
307+
})
308+
in := sampleModelCustomTypeWithPtrs()
309+
// when
310+
out := new(ModelWithUUIDs)
311+
if err := UnmarshalPayload(in, out); err != nil {
312+
t.Fatal(err)
313+
}
314+
// then
315+
if !out.ID.Equal(UUID{"12345678-abcd-1234-abcd-123456789012"}) {
316+
t.Fatalf("Did not set ID on dst interface: '%v'", out.ID)
317+
}
318+
if !out.UUIDField.Equal(UUID{"87654321-dcba-4321-dcba-210987654321"}) {
319+
t.Fatalf("Did not set UUIDField on dst interface: '%v'", out.UUIDField)
320+
}
321+
if !out.LatestRelatedModel.ID.Equal(UUID{"12345678-abcd-1234-abcd-111111111111"}) {
322+
t.Fatalf("Did not set LatestRelatedModel.ID on dst interface: '%v'", out.LatestRelatedModel.ID)
323+
}
324+
if !out.LatestRelatedModel.UUIDField.Equal(UUID{"87654321-dcba-4321-dcba-111111111111"}) {
325+
t.Fatalf("Did not set LatestRelatedModel.UUIDField on dst interface: '%v'", out.LatestRelatedModel.UUIDField)
326+
}
327+
if !out.RelatedModels[0].ID.Equal(UUID{"12345678-abcd-1234-abcd-222222222222"}) {
328+
t.Fatalf("Did not set RelatedModels[0].ID on dst interface: '%v'", out.LatestRelatedModel.ID)
329+
}
330+
if !out.RelatedModels[0].UUIDField.Equal(UUID{"87654321-dcba-4321-dcba-222222222222"}) {
331+
t.Fatalf("Did not set LatestRelatedModel.UUIDField on dst interface: '%v'", out.LatestRelatedModel.UUIDField)
332+
}
333+
}
334+
252335
func TestUnmarshalSetsAttrs(t *testing.T) {
253336
out, err := unmarshalSamplePayload()
254337
if err != nil {
@@ -945,3 +1028,67 @@ func sampleSerializedEmbeddedTestModel() *Blog {
9451028

9461029
return blog
9471030
}
1031+
1032+
func sampleModelCustomType() io.Reader {
1033+
model := sampleModelCustomTypePayload()
1034+
out := bytes.NewBuffer(nil)
1035+
err := MarshalPayload(out, model)
1036+
if err != nil {
1037+
fmt.Printf("Marshalled Custom Type failed: %s\n", err.Error())
1038+
}
1039+
fmt.Printf("Marshalled Custom Type: %s\n", out.String())
1040+
return out
1041+
}
1042+
1043+
func sampleModelCustomTypePayload() *ModelWithUUIDs {
1044+
return &ModelWithUUIDs{
1045+
ID: UUID{"12345678-abcd-1234-abcd-123456789012"},
1046+
UUIDField: UUID{"87654321-dcba-4321-dcba-210987654321"},
1047+
LatestRelatedModel: &RelatedModelWithUUIDs{
1048+
ID: UUID{"12345678-abcd-1234-abcd-111111111111"},
1049+
UUIDField: UUID{"87654321-dcba-4321-dcba-111111111111"},
1050+
},
1051+
RelatedModels: []*RelatedModelWithUUIDs{
1052+
&RelatedModelWithUUIDs{
1053+
ID: UUID{"12345678-abcd-1234-abcd-222222222222"},
1054+
UUIDField: UUID{"87654321-dcba-4321-dcba-222222222222"},
1055+
},
1056+
&RelatedModelWithUUIDs{
1057+
ID: UUID{"12345678-abcd-1234-abcd-333333333333"},
1058+
UUIDField: UUID{"87654321-dcba-4321-dcba-333333333333"},
1059+
},
1060+
},
1061+
}
1062+
}
1063+
1064+
func sampleModelCustomTypeWithPtrs() io.Reader {
1065+
model := sampleModelCustomTypeWithPtrs()
1066+
out := bytes.NewBuffer(nil)
1067+
err := MarshalPayload(out, model)
1068+
if err != nil {
1069+
fmt.Printf("Marshalled Custom Type failed: %s\n", err.Error())
1070+
}
1071+
fmt.Printf("Marshalled Custom Type (with pointers): '%s'\n", out.String())
1072+
return out
1073+
}
1074+
1075+
func sampleModelCustomTypeWithPtrsPayload() *ModelWithUUIDPtrs {
1076+
return &ModelWithUUIDPtrs{
1077+
ID: &UUID{"12345678-abcd-1234-abcd-123456789012"},
1078+
UUIDField: &UUID{"87654321-dcba-4321-dcba-210987654321"},
1079+
LatestRelatedModel: &RelatedModelWithUUIDPtrs{
1080+
ID: &UUID{"12345678-abcd-1234-abcd-111111111111"},
1081+
UUIDField: &UUID{"87654321-dcba-4321-dcba-111111111111"},
1082+
},
1083+
RelatedModels: []*RelatedModelWithUUIDPtrs{
1084+
&RelatedModelWithUUIDPtrs{
1085+
ID: &UUID{"12345678-abcd-1234-abcd-222222222222"},
1086+
UUIDField: &UUID{"87654321-dcba-4321-dcba-222222222222"},
1087+
},
1088+
&RelatedModelWithUUIDPtrs{
1089+
ID: &UUID{"12345678-abcd-1234-abcd-333333333333"},
1090+
UUIDField: &UUID{"87654321-dcba-4321-dcba-333333333333"},
1091+
},
1092+
},
1093+
}
1094+
}

0 commit comments

Comments
 (0)