From 91ece5cc8c91c892f5dd153a43579d8ee77829d7 Mon Sep 17 00:00:00 2001 From: Xavier Coulon Date: Wed, 13 Sep 2017 18:02:49 +0200 Subject: [PATCH] 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 --- custom_types.go | 42 ++++++++++++ custom_types_test.go | 25 ++++++++ models_test.go | 38 +++++++++++ request.go | 27 +++++++- request_test.go | 149 ++++++++++++++++++++++++++++++++++++++++++- response.go | 75 +++++++++++++--------- response_test.go | 105 ++++++++++++++++++++++++++++-- 7 files changed, 424 insertions(+), 37 deletions(-) create mode 100644 custom_types.go create mode 100644 custom_types_test.go diff --git a/custom_types.go b/custom_types.go new file mode 100644 index 0000000..3cbf448 --- /dev/null +++ b/custom_types.go @@ -0,0 +1,42 @@ +package jsonapi + +import "reflect" + +type MarshallingFunc func(interface{}) (string, error) +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() { + 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) +} diff --git a/custom_types_test.go b/custom_types_test.go new file mode 100644 index 0000000..5391947 --- /dev/null +++ b/custom_types_test.go @@ -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) + } + } +} diff --git a/models_test.go b/models_test.go index a53dd61..0d45f40 100644 --- a/models_test.go +++ b/models_test.go @@ -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"` diff --git a/request.go b/request.go index fe29706..8b73f00 100644 --- a/request.go +++ b/request.go @@ -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) } }() @@ -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 @@ -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. @@ -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 diff --git a/request_test.go b/request_test.go index 6b47fd7..2fb73e9 100644 --- a/request_test.go +++ b/request_test.go @@ -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{}{ @@ -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 { @@ -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 := sampleModelCustomTypeWithPtrs() + 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"}, + }, + }, + } +} diff --git a/response.go b/response.go index 76c3a3d..793db97 100644 --- a/response.go +++ b/response.go @@ -18,7 +18,7 @@ var ( // ErrBadJSONAPIID is returned when the Struct JSON API annotated "id" field // was not a valid numeric type. ErrBadJSONAPIID = errors.New( - "id should be either string, int(8,16,32,64) or uint(8,16,32,64)") + "id should be either a string, a registered custom type, an int(8,16,32,64) or an uint(8,16,32,64)") // ErrExpectedSlice is returned when a variable or arugment was expected to // be a slice of *Structs; MarshalMany will return this error when its // interface{} argument is invalid. @@ -248,35 +248,41 @@ func visitModelNode(model interface{}, included *map[string]*Node, kind = fieldType.Type.Kind() } - // Handle allowed types - switch kind { - case reflect.String: - node.ID = v.Interface().(string) - case reflect.Int: - node.ID = strconv.FormatInt(int64(v.Interface().(int)), 10) - case reflect.Int8: - node.ID = strconv.FormatInt(int64(v.Interface().(int8)), 10) - case reflect.Int16: - node.ID = strconv.FormatInt(int64(v.Interface().(int16)), 10) - case reflect.Int32: - node.ID = strconv.FormatInt(int64(v.Interface().(int32)), 10) - case reflect.Int64: - node.ID = strconv.FormatInt(v.Interface().(int64), 10) - case reflect.Uint: - node.ID = strconv.FormatUint(uint64(v.Interface().(uint)), 10) - case reflect.Uint8: - node.ID = strconv.FormatUint(uint64(v.Interface().(uint8)), 10) - case reflect.Uint16: - node.ID = strconv.FormatUint(uint64(v.Interface().(uint16)), 10) - case reflect.Uint32: - node.ID = strconv.FormatUint(uint64(v.Interface().(uint32)), 10) - case reflect.Uint64: - node.ID = strconv.FormatUint(v.Interface().(uint64), 10) - default: - // We had a JSON float (numeric), but our field was not one of the - // allowed numeric types - er = ErrBadJSONAPIID - break + if IsRegisteredType(fieldType.Type) { + marshallingFunc := customTypeMarshallingFuncs[fieldType.Type] + // we must not use 'v' here because we must keep the pointer indirection if it applies + node.ID, _ = marshallingFunc(fieldValue.Interface()) + } else { + // Handle allowed types + switch kind { + case reflect.String: + node.ID = v.Interface().(string) + case reflect.Int: + node.ID = strconv.FormatInt(int64(v.Interface().(int)), 10) + case reflect.Int8: + node.ID = strconv.FormatInt(int64(v.Interface().(int8)), 10) + case reflect.Int16: + node.ID = strconv.FormatInt(int64(v.Interface().(int16)), 10) + case reflect.Int32: + node.ID = strconv.FormatInt(int64(v.Interface().(int32)), 10) + case reflect.Int64: + node.ID = strconv.FormatInt(v.Interface().(int64), 10) + case reflect.Uint: + node.ID = strconv.FormatUint(uint64(v.Interface().(uint)), 10) + case reflect.Uint8: + node.ID = strconv.FormatUint(uint64(v.Interface().(uint8)), 10) + case reflect.Uint16: + node.ID = strconv.FormatUint(uint64(v.Interface().(uint16)), 10) + case reflect.Uint32: + node.ID = strconv.FormatUint(uint64(v.Interface().(uint32)), 10) + case reflect.Uint64: + node.ID = strconv.FormatUint(v.Interface().(uint64), 10) + default: + // We had a JSON float (numeric), but our field was not one of the + // allowed numeric types + er = ErrBadJSONAPIID + break + } } node.Type = args[1] @@ -348,6 +354,15 @@ func visitModelNode(model interface{}, included *map[string]*Node, strAttr, ok := fieldValue.Interface().(string) if ok { node.Attributes[args[1]] = strAttr + } else if IsRegisteredType(fieldValue.Type()) { + customMarshalFunc := customTypeMarshallingFuncs[fieldValue.Type()] + result, err := customMarshalFunc(fieldValue.Interface()) + if err != nil { + er = err + break + } else { + node.Attributes[args[1]] = result + } } else { node.Attributes[args[1]] = fieldValue.Interface() } diff --git a/response_test.go b/response_test.go index 71589dc..89b2465 100644 --- a/response_test.go +++ b/response_test.go @@ -3,6 +3,7 @@ package jsonapi import ( "bytes" "encoding/json" + "fmt" "reflect" "sort" "testing" @@ -250,7 +251,7 @@ func TestMarshalOnePayload_omitIDString(t *testing.T) { } } -func TestMarshall_invalidIDType(t *testing.T) { +func TestMarshal_invalidIDType(t *testing.T) { type badIDStruct struct { ID *bool `jsonapi:"primary,cars"` } @@ -259,12 +260,108 @@ func TestMarshall_invalidIDType(t *testing.T) { out := bytes.NewBuffer(nil) if err := MarshalPayload(out, o); err != ErrBadJSONAPIID { - t.Fatalf( - "Was expecting a `%s` error, got `%s`", ErrBadJSONAPIID, err, - ) + t.Fatalf("Was expecting a `%s` error, got `%s`", ErrBadJSONAPIID, err) } } +func TestMarshal_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 + }) + // declare the struct to marshall + payload := sampleModelCustomTypePayload() + // when + out := bytes.NewBuffer(nil) + err := MarshalPayload(out, payload) + // then + t.Logf("Output: %s", out.String()) + if err != nil { + t.Fatalf("Was expecting a no error, but got `%s`", err) + } + resp := new(OnePayload) + if err := json.NewDecoder(out).Decode(resp); err != nil { + t.Fatal(err) + } + data := resp.Data + if data.Type != "customtypes" { + t.Fatalf("type should have been `customtypes`, got `%s`", data.Type) + } + if data.ID != "12345678-abcd-1234-abcd-123456789012" { + t.Fatalf("ID not transfered") + } + if data.Attributes["uuid_field"] != "87654321-dcba-4321-dcba-210987654321" { + t.Fatalf("UUID attribute not marshalled as expected: '%v'", data.Attributes["uuid_field"]) + } +} +func TestMarshal_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) + }) + payload := sampleModelCustomTypeWithPtrsPayload() + // when + out := bytes.NewBuffer(nil) + err := MarshalPayload(out, payload) + // then + t.Logf("Output: %s", out.String()) + if err != nil { + t.Fatalf("Was expecting a no error, but got `%s`", err) + } + resp := new(OnePayload) + if err := json.NewDecoder(out).Decode(resp); err != nil { + t.Fatal(err) + } + data := resp.Data + if data.Type != "customtypes" { + t.Fatalf("type should have been `customtypes`, got `%s`", data.Type) + } + if data.ID != "12345678-abcd-1234-abcd-123456789012" { + t.Fatalf("ID not transfered as expected: '%v'", data.ID) + } + if data.Attributes["uuid_field"] != "87654321-dcba-4321-dcba-210987654321" { + t.Fatalf("UUID attribute not transfered as expected: '%v'", data.Attributes["uuid_field"]) + } + // verify the ids in the `relationships` section + latestRelatedModel := data.Relationships["latest_relatedmodel"] + latestRelatedModelData := latestRelatedModel.(map[string]interface{})["data"].(map[string]interface{}) + if latestRelatedModelData["id"].(string) != "12345678-abcd-1234-abcd-111111111111" { + t.Fatalf("latest_relatedmodel.data.id not transfered as expected: '%v'", latestRelatedModelData["id"]) + } + relatedModels := data.Relationships["relatedmodels"] + relatedModelsData := relatedModels.(map[string]interface{})["data"].([]interface{}) + if relatedModelsData[0].(map[string]interface{})["id"].(string) != "12345678-abcd-1234-abcd-222222222222" { + t.Fatalf("latest_relatedmodel.data.id not transfered as expected: '%v'", relatedModelsData[0].(map[string]interface{})["id"]) + } + // verify `uuid` attributes in the `included` section + for i, includedElement := range resp.Included { + if (includedElement.ID == "12345678-abcd-1234-abcd-111111111111" && includedElement.Attributes["uuid_field"] != "87654321-dcba-4321-dcba-111111111111") || + (includedElement.ID == "12345678-abcd-1234-abcd-222222222222" && includedElement.Attributes["uuid_field"] != "87654321-dcba-4321-dcba-222222222222") || + (includedElement.ID == "12345678-abcd-1234-abcd-333333333333" && includedElement.Attributes["uuid_field"] != "87654321-dcba-4321-dcba-333333333333") { + t.Fatalf("included.data[%d].attributes.uuid does not match: '%v'", i, includedElement.Attributes["uuid_field"]) + } + } + +} + func TestOmitsEmptyAnnotation(t *testing.T) { book := &Book{ Author: "aren55555",