Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support custom types (#114) #115

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 42 additions & 0 deletions custom_types.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package jsonapi

import "reflect"

type MarshallingFunc func(interface{}) (string, error)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Follow pattern of net/http's http.Handle and http.HandleFunc, and don't rely on input/output being string e.g.

type TypeMarshaller interface {
    Marshal(interface{}) (interface{}, error)
}

type TypeUnmarshaller interface {
    UnMarshal(interface{}) (interface{}, error)
}

type TypeMarshalFunc func(interface{}) (interface{}, error)

func (tmf TypeMarshalFunc) func(thing interface{}) (interface{}, error) {
    return tmf(thing)
}

type TypeUnmarshalFunc func(interface{}) (interface{}, error)

func (tuf TypeUnmarshalFunc) func(thing interface{}) (interface{}, error) {
    return tuf(thing)
}

Then you can have RegisterCustomType(in, out reflect.Type, marshaller TypeMarshaller, unmarshaller TypeUnmarshaller) and RegisterCustomTypeFunc(in, out reflect.Type, marshallerFunc TypeMarshallerFunc, unmarshallerFunc TypeUnmarshallerFunc)`

type UnmarshallingFunc func(string) (interface{}, error)

// map of functions to use to convert the field value into a JSON string
var customTypeMarshallingFuncs map[reflect.Type]MarshallingFunc

// map of functions to use to convert the JSON string value into the target field
var customTypeUnmarshallingFuncs map[reflect.Type]UnmarshallingFunc

// init initializes the maps
func init() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't use init(), don't use make, use var in block, e.g.

var (
    customTypeMarshallingFuncs = map[reflect.Type]MarshallingFunc{}
    cutomTypeUnmarshallingFuncs = map[reflect.Type]UnmarshallingFunc{}
)

See,
https://play.golang.org/p/OnktgftaGQ

customTypeMarshallingFuncs = make(map[reflect.Type]MarshallingFunc, 0)
customTypeUnmarshallingFuncs = make(map[reflect.Type]UnmarshallingFunc, 0)
}

// IsRegisteredType checks if the given type `t` is registered as a custom type
func IsRegisteredType(t reflect.Type) bool {
_, ok := customTypeMarshallingFuncs[t]
return ok
}

// RegisterType registers the functions to convert the field from a custom type to a string and vice-versa
// in the JSON requests/responses.
// The `marshallingFunc` must be a function that returns a string (along with an error if something wrong happened)
// and the `unmarshallingFunc` must be a function that takes
// a string as its sole argument and return an instance of `typeName` (along with an error if something wrong happened).
// Eg: `uuid.FromString(string) uuid.UUID {...} and `uuid.String() string {...}
func RegisterType(customType reflect.Type, marshallingFunc MarshallingFunc, unmarshallingFunc UnmarshallingFunc) {
// register the pointer to the type
customTypeMarshallingFuncs[customType] = marshallingFunc
customTypeUnmarshallingFuncs[customType] = unmarshallingFunc
}

// resetCustomTypeRegistrations resets the custom type registration, which is useful during testing
func resetCustomTypeRegistrations() {
customTypeMarshallingFuncs = make(map[reflect.Type]MarshallingFunc, 0)
customTypeUnmarshallingFuncs = make(map[reflect.Type]UnmarshallingFunc, 0)
}
25 changes: 25 additions & 0 deletions custom_types_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package jsonapi

import (
"reflect"
"testing"
)

func TestRegisterCustomTypes(t *testing.T) {
for _, uuidType := range []reflect.Type{reflect.TypeOf(UUID{}), reflect.TypeOf(&UUID{})} {
// given
resetCustomTypeRegistrations() // make sure no other registration interferes with this test
// when
RegisterType(uuidType,
func(value interface{}) (string, error) {
return "", nil
},
func(value string) (interface{}, error) {
return nil, nil
})
// then
if !IsRegisteredType(uuidType) {
t.Fatalf("Expected `%v` to be registered but it was not", uuidType)
}
}
}
38 changes: 38 additions & 0 deletions models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,44 @@ type ModelBadTypes struct {
TimePtrField *time.Time `jsonapi:"attr,time_ptr_field"`
}

type ModelWithUUIDs struct {
ID UUID `jsonapi:"primary,customtypes"`
UUIDField UUID `jsonapi:"attr,uuid_field"`
LatestRelatedModel *RelatedModelWithUUIDs `jsonapi:"relation,latest_relatedmodel"`
RelatedModels []*RelatedModelWithUUIDs `jsonapi:"relation,relatedmodels"`
}
type RelatedModelWithUUIDs struct {
ID UUID `jsonapi:"primary,relatedtypes"`
UUIDField UUID `jsonapi:"attr,uuid_field"`
}

type ModelWithUUIDPtrs struct {
ID *UUID `jsonapi:"primary,customtypes"`
UUIDField *UUID `jsonapi:"attr,uuid_field"`
LatestRelatedModel *RelatedModelWithUUIDPtrs `jsonapi:"relation,latest_relatedmodel"`
RelatedModels []*RelatedModelWithUUIDPtrs `jsonapi:"relation,relatedmodels"`
}

type RelatedModelWithUUIDPtrs struct {
ID *UUID `jsonapi:"primary,relatedtypes"`
UUIDField *UUID `jsonapi:"attr,uuid_field"`
}

type UUID struct {
string
}

func UUIDFromString(s string) (*UUID, error) {
return &UUID{s}, nil
}
func (u UUID) String() string {
return u.string
}

func (u UUID) Equal(other UUID) bool {
return u.string == other.string
}

type WithPointer struct {
ID *uint64 `jsonapi:"primary,with-pointers"`
Name *string `jsonapi:"attr,name"`
Expand Down
27 changes: 25 additions & 2 deletions request.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ func UnmarshalManyPayload(in io.Reader, t reflect.Type) ([]interface{}, error) {
func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("data is not a jsonapi representation of '%v'", model.Type())
err = fmt.Errorf("data is not a jsonapi representation of '%v': %v", model.Type(), r)
}
}()

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

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

// Deal with PTRS
Expand All @@ -184,6 +184,17 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
assign(fieldValue, v)
continue
}
// custom type that can be unmarshalled from a string
if v.Kind() == reflect.String && IsRegisteredType(fieldType.Type) {
unmashalFunc := customTypeUnmarshallingFuncs[fieldType.Type]
r, err := unmashalFunc(data.ID)
if err != nil {
er = err
} else {
fieldValue.Set(reflect.ValueOf(r))
}
continue
}

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

// custom type that can be unmarshalled from a string
if v.Kind() == reflect.String && IsRegisteredType(fieldType.Type) {
unmashalFunc := customTypeUnmarshallingFuncs[fieldType.Type]
r, err := unmashalFunc(val.(string))
if err != nil {
er = err
} else {
fieldValue.Set(reflect.ValueOf(r))
}
continue
}

// Field was a Pointer type
if fieldValue.Kind() == reflect.Ptr {
var concreteVal reflect.Value
Expand Down
149 changes: 148 additions & 1 deletion request_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (
"time"
)

func TestUnmarshall_attrStringSlice(t *testing.T) {
func TestUnmarshal_attrStringSlice(t *testing.T) {
out := &Book{}
tags := []string{"fiction", "sale"}
data := map[string]interface{}{
Expand Down Expand Up @@ -249,6 +249,89 @@ func TestUnmarshal_nonNumericID(t *testing.T) {
}
}

func TestUnmarshal_CustomType(t *testing.T) {
// given
// register the custom `UUID` type
uuidType := reflect.TypeOf(UUID{})
RegisterType(uuidType,
func(value interface{}) (string, error) {
return value.(UUID).String(), nil
},
func(value string) (interface{}, error) {
result, err := UUIDFromString(value)
if err != nil {
fmt.Println("Error while converting from string to UUID: " + err.Error())
return nil, err
}
return *result, nil
})
in := sampleModelCustomType()
// when
out := new(ModelWithUUIDs)
if err := UnmarshalPayload(in, out); err != nil {
t.Fatal(err)
}
// then
if !out.ID.Equal(UUID{"12345678-abcd-1234-abcd-123456789012"}) {
t.Fatalf("Did not set ID on dst interface: '%v'", out.ID)
}
if !out.UUIDField.Equal(UUID{"87654321-dcba-4321-dcba-210987654321"}) {
t.Fatalf("Did not set UUIDField on dst interface: '%v'", out.UUIDField)
}
if !out.LatestRelatedModel.ID.Equal(UUID{"12345678-abcd-1234-abcd-111111111111"}) {
t.Fatalf("Did not set LatestRelatedModel.ID on dst interface: '%v'", out.LatestRelatedModel.ID)
}
if !out.LatestRelatedModel.UUIDField.Equal(UUID{"87654321-dcba-4321-dcba-111111111111"}) {
t.Fatalf("Did not set LatestRelatedModel.UUIDField on dst interface: '%v'", out.LatestRelatedModel.UUIDField)
}
if !out.RelatedModels[0].ID.Equal(UUID{"12345678-abcd-1234-abcd-222222222222"}) {
t.Fatalf("Did not set RelatedModels[0].ID on dst interface: '%v'", out.LatestRelatedModel.ID)
}
if !out.RelatedModels[0].UUIDField.Equal(UUID{"87654321-dcba-4321-dcba-222222222222"}) {
t.Fatalf("Did not set LatestRelatedModel.UUIDField on dst interface: '%v'", out.LatestRelatedModel.UUIDField)
}

}

func TestUnmarshal_CustomType_Ptr(t *testing.T) {
// given
// register the custom `*UUID` type
uuidType := reflect.TypeOf(&UUID{})
RegisterType(uuidType,
func(value interface{}) (string, error) {
result := value.(*UUID).String()
return result, nil
},
func(value string) (interface{}, error) {
return UUIDFromString(value)
})
in := sampleModelCustomTypeWithPtrs()
// when
out := new(ModelWithUUIDs)
if err := UnmarshalPayload(in, out); err != nil {
t.Fatal(err)
}
// then
if !out.ID.Equal(UUID{"12345678-abcd-1234-abcd-123456789012"}) {
t.Fatalf("Did not set ID on dst interface: '%v'", out.ID)
}
if !out.UUIDField.Equal(UUID{"87654321-dcba-4321-dcba-210987654321"}) {
t.Fatalf("Did not set UUIDField on dst interface: '%v'", out.UUIDField)
}
if !out.LatestRelatedModel.ID.Equal(UUID{"12345678-abcd-1234-abcd-111111111111"}) {
t.Fatalf("Did not set LatestRelatedModel.ID on dst interface: '%v'", out.LatestRelatedModel.ID)
}
if !out.LatestRelatedModel.UUIDField.Equal(UUID{"87654321-dcba-4321-dcba-111111111111"}) {
t.Fatalf("Did not set LatestRelatedModel.UUIDField on dst interface: '%v'", out.LatestRelatedModel.UUIDField)
}
if !out.RelatedModels[0].ID.Equal(UUID{"12345678-abcd-1234-abcd-222222222222"}) {
t.Fatalf("Did not set RelatedModels[0].ID on dst interface: '%v'", out.LatestRelatedModel.ID)
}
if !out.RelatedModels[0].UUIDField.Equal(UUID{"87654321-dcba-4321-dcba-222222222222"}) {
t.Fatalf("Did not set LatestRelatedModel.UUIDField on dst interface: '%v'", out.LatestRelatedModel.UUIDField)
}
}

func TestUnmarshalSetsAttrs(t *testing.T) {
out, err := unmarshalSamplePayload()
if err != nil {
Expand Down Expand Up @@ -945,3 +1028,67 @@ func sampleSerializedEmbeddedTestModel() *Blog {

return blog
}

func sampleModelCustomType() io.Reader {
model := sampleModelCustomTypePayload()
out := bytes.NewBuffer(nil)
err := MarshalPayload(out, model)
if err != nil {
fmt.Printf("Marshalled Custom Type failed: %s\n", err.Error())
}
fmt.Printf("Marshalled Custom Type: %s\n", out.String())
return out
}

func sampleModelCustomTypePayload() *ModelWithUUIDs {
return &ModelWithUUIDs{
ID: UUID{"12345678-abcd-1234-abcd-123456789012"},
UUIDField: UUID{"87654321-dcba-4321-dcba-210987654321"},
LatestRelatedModel: &RelatedModelWithUUIDs{
ID: UUID{"12345678-abcd-1234-abcd-111111111111"},
UUIDField: UUID{"87654321-dcba-4321-dcba-111111111111"},
},
RelatedModels: []*RelatedModelWithUUIDs{
&RelatedModelWithUUIDs{
ID: UUID{"12345678-abcd-1234-abcd-222222222222"},
UUIDField: UUID{"87654321-dcba-4321-dcba-222222222222"},
},
&RelatedModelWithUUIDs{
ID: UUID{"12345678-abcd-1234-abcd-333333333333"},
UUIDField: UUID{"87654321-dcba-4321-dcba-333333333333"},
},
},
}
}

func sampleModelCustomTypeWithPtrs() io.Reader {
model := sampleModelCustomTypeWithPtrsPayload()
out := bytes.NewBuffer(nil)
err := MarshalPayload(out, model)
if err != nil {
fmt.Printf("Marshalled Custom Type failed: %s\n", err.Error())
}
fmt.Printf("Marshalled Custom Type (with pointers): '%s'\n", out.String())
return out
}

func sampleModelCustomTypeWithPtrsPayload() *ModelWithUUIDPtrs {
return &ModelWithUUIDPtrs{
ID: &UUID{"12345678-abcd-1234-abcd-123456789012"},
UUIDField: &UUID{"87654321-dcba-4321-dcba-210987654321"},
LatestRelatedModel: &RelatedModelWithUUIDPtrs{
ID: &UUID{"12345678-abcd-1234-abcd-111111111111"},
UUIDField: &UUID{"87654321-dcba-4321-dcba-111111111111"},
},
RelatedModels: []*RelatedModelWithUUIDPtrs{
&RelatedModelWithUUIDPtrs{
ID: &UUID{"12345678-abcd-1234-abcd-222222222222"},
UUIDField: &UUID{"87654321-dcba-4321-dcba-222222222222"},
},
&RelatedModelWithUUIDPtrs{
ID: &UUID{"12345678-abcd-1234-abcd-333333333333"},
UUIDField: &UUID{"87654321-dcba-4321-dcba-333333333333"},
},
},
}
}
Loading