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 extending model with anonymous field #87

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
Binary file added .helper_test.go.swp
Binary file not shown.
Binary file added .models_test.go.swp
Binary file not shown.
Binary file added .request_test.go.swp
Binary file not shown.
1 change: 1 addition & 0 deletions constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const (
annotationOmitEmpty = "omitempty"
annotationISO8601 = "iso8601"
annotationSeperator = ","
annotationExtend = "extend"

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

Expand Down
65 changes: 65 additions & 0 deletions helper.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package jsonapi

import (
"reflect"
"strings"
)

type structExtractedField struct {
reflect.Value
reflect.Kind
Annotation string
Args []string
IsPtr bool
}

func extractFields(model reflect.Value) ([]structExtractedField, error) {
modelValue := model.Elem()
modelType := model.Type().Elem()

var fields [](structExtractedField)

for i := 0; i < modelValue.NumField(); i++ {
structField := modelValue.Type().Field(i)
fieldValue := modelValue.Field(i)
fieldType := modelType.Field(i)

tag := structField.Tag.Get(annotationJSONAPI)
if tag == "" {
continue
}
if tag == annotationExtend && fieldType.Anonymous {
extendedFields, er := extractFields(modelValue.Field(i).Addr())
if er != nil {
return nil, er
}
fields = append(fields, extendedFields...)
continue
}

args := strings.Split(tag, annotationSeperator)

if len(args) < 1 {
return nil, ErrBadJSONAPIStructTag
}

annotation, args := args[0], args[1:]

if (annotation == annotationClientID && len(args) != 0) ||
(annotation != annotationClientID && len(args) < 1) {
return nil, ErrBadJSONAPIStructTag
}

// Deal with PTRS
kind := fieldValue.Kind()
isPtr := fieldValue.Kind() == reflect.Ptr
if isPtr {
kind = fieldType.Type.Elem().Kind()
}

field := structExtractedField{Value: fieldValue, Kind: kind, Annotation: annotation, Args: args, IsPtr: isPtr}
fields = append(fields, field)
}

return fields, nil
}
187 changes: 187 additions & 0 deletions helper_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package jsonapi

import (
"reflect"
"testing"
"time"
"unsafe"
)

func TestHelper_BadPrimaryAnnotation(t *testing.T) {
fields, err := extractFields(reflect.ValueOf(new(BadModel)))

if fields != nil {
t.Fatalf("Was expecting results to be nil")
}

if expected, actual := ErrBadJSONAPIStructTag, err; expected != actual {
t.Fatalf("Was expecting error to be `%s`, got `%s`", expected, actual)
}
}

func TestHelper_BadExtendedAnonymousField(t *testing.T) {
fields, err := extractFields(reflect.ValueOf(new(WithBadExtendedAnonymousField)))

if fields != nil {
t.Fatalf("Was expecting results to be nil")
}

if expected, actual := ErrBadJSONAPIStructTag, err; expected != actual {
t.Fatalf("Was expecting error to be `%s`, got `%s`", expected, actual)
}
}

func TestHelper_returnsProperValue(t *testing.T) {
comment := &Comment{}
fields, err := extractFields(reflect.ValueOf(comment))

if err != nil {
t.Fatalf("Was expecting error to be nil, got `%s`", err)
}

if expected, actual := 4, len(fields); expected != actual {
t.Fatalf("Was expecting fields to have `%d` items, got `%d`", expected, actual)
}

// Check Annotation value
if expected, actual := "primary", fields[0].Annotation; expected != actual {
t.Fatalf("Was expecting fields[0].Annotation to be `%s`, got `%s`", expected, actual)
}

if expected, actual := "client-id", fields[1].Annotation; expected != actual {
t.Fatalf("Was expecting fields[1].Annotation to be `%s`, got `%s`", expected, actual)
}

if expected, actual := "attr", fields[2].Annotation; expected != actual {
t.Fatalf("Was expecting fields[2].Annotation to be `%s`, got `%s`", expected, actual)
}

if expected, actual := "attr", fields[3].Annotation; expected != actual {
t.Fatalf("Was expecting fields[3].Annotation to be `%s`, got `%s`", expected, actual)
}

// Check Args value
if expected, actual := []string{"comments"}, fields[0].Args; !reflect.DeepEqual(expected, actual) {
t.Fatalf("Was expecting fields[0].Args to be `%s`, got `%s`", expected, actual)
}

if expected, actual := []string{}, fields[1].Args; !reflect.DeepEqual(expected, actual) {
t.Fatalf("Was expecting fields[1].Args to be `%s`, got `%s`", expected, actual)
}

if expected, actual := []string{"post_id"}, fields[2].Args; !reflect.DeepEqual(expected, actual) {
t.Fatalf("Was expecting fields[2].Args to be `%s`, got `%s`", expected, actual)
}

if expected, actual := []string{"body"}, fields[3].Args; !reflect.DeepEqual(expected, actual) {
t.Fatalf("Was expecting fields[3].Args to be `%s`, got `%s`", expected, actual)
}

// Check IsPtr
if expected, actual := false, fields[0].IsPtr; !reflect.DeepEqual(expected, actual) {
t.Fatalf("Was expecting fields[0].IsPtr to be `%t`, got `%t`", expected, actual)
}

if expected, actual := false, fields[1].IsPtr; !reflect.DeepEqual(expected, actual) {
t.Fatalf("Was expecting fields[1].IsPtr to be `%t`, got `%t`", expected, actual)
}

if expected, actual := false, fields[2].IsPtr; !reflect.DeepEqual(expected, actual) {
t.Fatalf("Was expecting fields[2].IsPtr to be `%t`, got `%t`", expected, actual)
}

if expected, actual := false, fields[3].IsPtr; !reflect.DeepEqual(expected, actual) {
t.Fatalf("Was expecting fields[3].IsPtr to be `%t`, got `%t`", expected, actual)
}

// Check Value value
if uintptr(unsafe.Pointer(&comment.ID)) != fields[0].Value.UnsafeAddr() {
t.Fatalf("Was expecting fields[0].Value to point to comment.ID")
}

if uintptr(unsafe.Pointer(&comment.ClientID)) != fields[1].Value.UnsafeAddr() {
t.Fatalf("Was expecting fields[1].Value to point to comment.ClientID")
}

if uintptr(unsafe.Pointer(&comment.PostID)) != fields[2].Value.UnsafeAddr() {
t.Fatalf("Was expecting fields[2].Value to point to comment.PostID")
}

if uintptr(unsafe.Pointer(&comment.Body)) != fields[3].Value.UnsafeAddr() {
t.Fatalf("Was expecting fields[3].Value to point to comment.Body")
}

// Check Kind value
if expected, actual := reflect.Int, fields[0].Kind; expected != actual {
t.Fatalf("Was expecting fields[0].Kind to be `%s`, got `%s`", expected, actual)
}

if expected, actual := reflect.String, fields[1].Kind; expected != actual {
t.Fatalf("Was expecting fields[1].Kind to be `%s`, got `%s`", expected, actual)
}

if expected, actual := reflect.Int, fields[2].Kind; expected != actual {
t.Fatalf("Was expecting fields[2].Kind to be `%s`, got `%s`", expected, actual)
}

if expected, actual := reflect.String, fields[3].Kind; expected != actual {
t.Fatalf("Was expecting fields[3].Kind to be `%s`, got `%s`", expected, actual)
}

}

func TestHelper_ignoreFieldWithoutAnnotation(t *testing.T) {
book := &Book{
ID: 0,
Author: "aren55555",
PublishedAt: time.Now().AddDate(0, -1, 0),
}
fields, err := extractFields(reflect.ValueOf(book))
if err != nil {
t.Fatalf("Was expecting error to be nil, got `%s`", err)
}

if expected, actual := 7, len(fields); expected != actual {
t.Fatalf("Was expecting fields to have `%d` items, got `%d`", expected, actual)
}
}

func TestHelper_WithExtendedAnonymousField(t *testing.T) {
model := &WithExtendedAnonymousField{}
fields, err := extractFields(reflect.ValueOf(model))
if err != nil {
t.Fatalf("Was expecting error to be nil, got `%s`", err)
}

if expected, actual := 2, len(fields); expected != actual {
t.Fatalf("Was expecting fields to have `%d` items, got `%d`", expected, actual)
}

if uintptr(unsafe.Pointer(&model.CommonField)) != fields[0].Value.UnsafeAddr() {
t.Fatalf("Was expecting fields[0].Value to point to comment.CommonField")
}

if uintptr(unsafe.Pointer(&model.ID)) != fields[1].Value.UnsafeAddr() {
t.Fatalf("Was expecting fields[1].Value to point to comment.ID")
}
}

func TestHelper_WithPointer(t *testing.T) {
model := &WithPointer{}
fields, err := extractFields(reflect.ValueOf(model))
if err != nil {
t.Fatalf("Was expecting error to be nil, got `%s`", err)
}

if expected, actual := 5, len(fields); expected != actual {
t.Fatalf("Was expecting fields to have `%d` items, got `%d`", expected, actual)
}

if expected, actual := true, fields[0].IsPtr; !reflect.DeepEqual(expected, actual) {
t.Fatalf("Was expecting fields[0].IsPtr to be `%t`, got `%t`", expected, actual)
}

if uintptr(unsafe.Pointer(&model.ID)) != fields[0].Value.UnsafeAddr() {
t.Fatalf("Was expecting fields[0].Value to point to comment.ID")
}
}
18 changes: 18 additions & 0 deletions models_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,24 @@ type WithPointer struct {
FloatVal *float32 `jsonapi:"attr,float-val"`
}

type base struct {
CommonField string `jsonapi:"attr,common_field"`
}

type WithExtendedAnonymousField struct {
base `jsonapi:"extend"`
ID int `jsonapi:"primary,with-extended-anonymous-fields"`
}

type badBase struct {
CommonField string `jsonapi:"attr"`
}

type WithBadExtendedAnonymousField struct {
badBase `jsonapi:"extend"`
ID int `jsonapi:"primary,with-bad-extended-anonymous-fields"`
}

type Timestamp struct {
ID int `jsonapi:"primary,timestamps"`
Time time.Time `jsonapi:"attr,timestamp,iso8601"`
Expand Down
Loading