From 9d5af7fa20c902a6b78959716afbeefd53f3bb8b Mon Sep 17 00:00:00 2001
From: Mylan Connolly <mconnolly@iotrak.com>
Date: Wed, 8 Nov 2017 13:29:06 -0500
Subject: [PATCH 1/2] Initial implementation of encoding integers to strings

---
 constants.go     |  1 +
 models_test.go   |  8 ++++++++
 request.go       | 44 ++++++++++++++++++++++++++++++++++++++++++--
 request_test.go  | 36 ++++++++++++++++++++++++++++++++++++
 response.go      | 46 +++++++++++++++++++++++++++++++++++++++++-----
 response_test.go | 41 +++++++++++++++++++++++++++++++++++++++++
 6 files changed, 169 insertions(+), 7 deletions(-)

diff --git a/constants.go b/constants.go
index 23288d3..e3dc092 100644
--- a/constants.go
+++ b/constants.go
@@ -9,6 +9,7 @@ const (
 	annotationRelation  = "relation"
 	annotationOmitEmpty = "omitempty"
 	annotationISO8601   = "iso8601"
+	annotationString    = "string"
 	annotationSeperator = ","
 
 	iso8601TimeFormat = "2006-01-02T15:04:05Z"
diff --git a/models_test.go b/models_test.go
index a53dd61..7b789b7 100644
--- a/models_test.go
+++ b/models_test.go
@@ -31,6 +31,14 @@ type Timestamp struct {
 	Next *time.Time `jsonapi:"attr,next,iso8601"`
 }
 
+type StringInt struct {
+	ID             int    `jsonapi:"primary,string-ints"`
+	ParentID       int    `jsonapi:"attr,parent-id,string"`
+	OptParentID    *int   `jsonapi:"attr,opt-parent-id,string"`
+	BigParentID    int64  `jsonapi:"attr,big-parent-id,string"`
+	OptBigParentID *int64 `jsonapi:"attr,opt-big-parent-id,string"`
+}
+
 type Car struct {
 	ID    *string `jsonapi:"primary,cars"`
 	Make  *string `jsonapi:"attr,make,omitempty"`
diff --git a/request.go b/request.go
index fe29706..f4ca70b 100644
--- a/request.go
+++ b/request.go
@@ -32,6 +32,9 @@ var (
 	ErrUnsupportedPtrType = errors.New("Pointer type in struct is not supported")
 	// ErrInvalidType is returned when the given type is incompatible with the expected type.
 	ErrInvalidType = errors.New("Invalid type provided") // I wish we used punctuation.
+	// ErrInvalidNumberString is returned when the JSON value was a string, but
+	// could not be parsed as a number.
+	ErrInvalidNumberString = errors.New("The string value could not be parsed as a number")
 )
 
 // UnmarshalPayload converts an io into a struct instance using jsonapi tags on
@@ -248,12 +251,15 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
 				continue
 			}
 
-			var iso8601 bool
+			var iso8601, str bool
 
 			if len(args) > 2 {
 				for _, arg := range args[2:] {
-					if arg == annotationISO8601 {
+					switch arg {
+					case annotationISO8601:
 						iso8601 = true
+					case annotationString:
+						str = true
 					}
 				}
 			}
@@ -418,6 +424,40 @@ func unmarshalNode(data *Node, model reflect.Value, included *map[string]*Node)
 				continue
 			}
 
+			// We have a string and there is a `string` struct tag on the model we're
+			// unmarshalling.
+			if str && v.Kind() == reflect.String {
+				var numericValue reflect.Value
+
+				stringVal := v.Interface().(string)
+
+				// The field may or may not be a pointer to a numeric; the kind var
+				// will not contain a pointer type
+				var kind reflect.Kind
+				if fieldValue.Kind() == reflect.Ptr {
+					kind = fieldType.Type.Elem().Kind()
+				} else {
+					kind = fieldType.Type.Kind()
+				}
+
+				switch kind {
+				case reflect.Int64:
+					n, err := strconv.ParseInt(stringVal, 10, 64)
+					if err != nil {
+						return ErrInvalidNumberString
+					}
+					numericValue = reflect.ValueOf(&n)
+				case reflect.Int:
+					n, err := strconv.Atoi(stringVal)
+					if err != nil {
+						return ErrInvalidNumberString
+					}
+					numericValue = reflect.ValueOf(&n)
+				}
+				assign(fieldValue, numericValue)
+				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 2206449..abec340 100644
--- a/request_test.go
+++ b/request_test.go
@@ -290,6 +290,42 @@ func TestUnmarshalParsesISO8601(t *testing.T) {
 	}
 }
 
+func TestUnmarshalParsesStringInts(t *testing.T) {
+	payload := &OnePayload{
+		Data: &Node{
+			Type: "string-ints",
+			Attributes: map[string]interface{}{
+				"parent-id":         "2",
+				"opt-parent-id":     "3",
+				"big-parent-id":     "1362085645204177151",
+				"opt-big-parent-id": "1362085645204177151",
+			},
+		},
+	}
+
+	in := bytes.NewBuffer(nil)
+	json.NewEncoder(in).Encode(payload)
+
+	out := new(StringInt)
+
+	if err := UnmarshalPayload(in, out); err != nil {
+		t.Fatal(err)
+	}
+
+	if out.ParentID != 2 {
+		t.Fatal("Parsing the string encoded int failed")
+	}
+	if *out.OptParentID != 3 {
+		t.Fatal("Parsing the string encoded *int failed")
+	}
+	if out.BigParentID != 1362085645204177151 {
+		t.Fatal("Parsing the string encoded int64 failed")
+	}
+	if *out.OptBigParentID != 1362085645204177151 {
+		t.Fatal("Parsing the string encoded *int64 failed")
+	}
+}
+
 func TestUnmarshalParsesISO8601TimePointer(t *testing.T) {
 	payload := &OnePayload{
 		Data: &Node{
diff --git a/response.go b/response.go
index bbe6d2c..3d7c164 100644
--- a/response.go
+++ b/response.go
@@ -286,7 +286,7 @@ func visitModelNode(model interface{}, included *map[string]*Node,
 				node.ClientID = clientID
 			}
 		} else if annotation == annotationAttribute {
-			var omitEmpty, iso8601 bool
+			var omitEmpty, iso8601, str bool
 
 			if len(args) > 2 {
 				for _, arg := range args[2:] {
@@ -295,6 +295,8 @@ func visitModelNode(model interface{}, included *map[string]*Node,
 						omitEmpty = true
 					case annotationISO8601:
 						iso8601 = true
+					case annotationString:
+						str = true
 					}
 				}
 			}
@@ -345,10 +347,44 @@ func visitModelNode(model interface{}, included *map[string]*Node,
 					continue
 				}
 
-				strAttr, ok := fieldValue.Interface().(string)
-				if ok {
-					node.Attributes[args[1]] = strAttr
-				} else {
+				// Modify the output if the user specified that we need to string encode
+				// integer types.
+				switch attr := fieldValue.Interface().(type) {
+				case string:
+					node.Attributes[args[1]] = attr
+				case *int:
+					if attr == nil {
+						node.Attributes[args[1]] = nil
+					} else {
+						if str {
+							node.Attributes[args[1]] = strconv.Itoa(*attr)
+						} else {
+							node.Attributes[args[1]] = fieldValue.Interface()
+						}
+					}
+				case int:
+					if str {
+						node.Attributes[args[1]] = strconv.Itoa(attr)
+					} else {
+						node.Attributes[args[1]] = fieldValue.Interface()
+					}
+				case *int64:
+					if attr == nil {
+						node.Attributes[args[1]] = nil
+					} else {
+						if str {
+							node.Attributes[args[1]] = strconv.FormatInt(*attr, 10)
+						} else {
+							node.Attributes[args[1]] = fieldValue.Interface()
+						}
+					}
+				case int64:
+					if str {
+						node.Attributes[args[1]] = strconv.FormatInt(attr, 10)
+					} else {
+						node.Attributes[args[1]] = fieldValue.Interface()
+					}
+				default:
 					node.Attributes[args[1]] = fieldValue.Interface()
 				}
 			}
diff --git a/response_test.go b/response_test.go
index 9b0bf57..62ade60 100644
--- a/response_test.go
+++ b/response_test.go
@@ -413,6 +413,47 @@ func TestMarshalISO8601Time(t *testing.T) {
 	}
 }
 
+func TestMarshalIntString(t *testing.T) {
+	optParentID := 2
+	optBigParentID := int64(1362085645204177151)
+	testModel := &StringInt{
+		ID:             5,
+		ParentID:       1,
+		OptParentID:    &optParentID,
+		BigParentID:    1362085645204177151,
+		OptBigParentID: &optBigParentID,
+	}
+
+	out := bytes.NewBuffer(nil)
+	if err := MarshalPayload(out, testModel); err != nil {
+		t.Fatal(err)
+	}
+
+	resp := new(OnePayload)
+	if err := json.NewDecoder(out).Decode(resp); err != nil {
+		t.Fatal(err)
+	}
+
+	data := resp.Data
+
+	if data.Attributes == nil {
+		t.Fatalf("Expected attributes")
+	}
+
+	if data.Attributes["parent-id"] != "1" {
+		t.Fatal("ParentID was not serialised as a string correctly")
+	}
+	if data.Attributes["opt-parent-id"] != "2" {
+		t.Fatal("OptParentID was not serialised as a string correctly")
+	}
+	if data.Attributes["big-parent-id"] != "1362085645204177151" {
+		t.Fatal("BigParentID was not serialised as a string correctly")
+	}
+	if data.Attributes["opt-big-parent-id"] != "1362085645204177151" {
+		t.Fatal("OptBigParentID was not serialised as a string correctly")
+	}
+}
+
 func TestMarshalISO8601TimePointer(t *testing.T) {
 	tm := time.Date(2016, 8, 17, 8, 27, 12, 23849, time.UTC)
 	testModel := &Timestamp{

From e5020f0de3fa4d3cec5e342a2dc6a9e4e4a2dcf1 Mon Sep 17 00:00:00 2001
From: Mylan Connolly <mconnolly@iotrak.com>
Date: Thu, 9 Nov 2017 08:11:57 -0500
Subject: [PATCH 2/2] Added test case for invalid strings

---
 request_test.go | 22 ++++++++++++++++++++++
 1 file changed, 22 insertions(+)

diff --git a/request_test.go b/request_test.go
index abec340..22809de 100644
--- a/request_test.go
+++ b/request_test.go
@@ -326,6 +326,28 @@ func TestUnmarshalParsesStringInts(t *testing.T) {
 	}
 }
 
+func TestUnmarshalParsesStringIntInvalidInputs(t *testing.T) {
+	payload := &OnePayload{
+		Data: &Node{
+			Type: "string-ints",
+			Attributes: map[string]interface{}{
+				"parent-id":     "one",
+				"opt-parent-id": "Lorem ipsum",
+			},
+		},
+	}
+
+	in := bytes.NewBuffer(nil)
+	json.NewEncoder(in).Encode(payload)
+
+	out := new(StringInt)
+	err := UnmarshalPayload(in, out)
+
+	if err == nil || err != ErrInvalidNumberString {
+		t.Fatal("Parsing did not give the proper error message for an invalid string")
+	}
+}
+
 func TestUnmarshalParsesISO8601TimePointer(t *testing.T) {
 	payload := &OnePayload{
 		Data: &Node{