Repository: line/line-bot-sdk-go
Pull request #346 (2023-05-06 03:50:27)

Add unmarshal json for template
### Motivation

Resolve a task #345 
This is to add `UnmarshalJSON` functionality to `TemplateMessage`.

### Description of the changes

- `template_unmarshal.go` file and created the `UnmarshalTemplateJSON` method. The logic of this method is almost identical to `UnmarshalFlexMessageJSON` method.
- Add test code to `UnmarshalFlexMessageJSON` (`template_test.go`)

### What we considered but did not do
While implementing `UnmarshalTemplateJSON`, I noticed that `UnmarshalJSON` is not implemented for `CarouselColumn` and `ImageCarouselColumn`. However, considering the possibility that it might have a bad influence on other codes, we added `rawColumn` and `rawImageColumn` here and implemented `UnmarshalJSON`. If there is no risk of having a bad influence on other code, modify `CarouselColumn` and `ImageCarouselColumn` to add `UnmarshalJSON`.

### What to do next

There was not enough testing for `MarshalJSON` in `Template`, so we will add it.


Patch:
diff --git a/linebot/template_unmarshal.go b/linebot/template_unmarshal.go
new file mode 100644
index 0000000..07aca05
--- /dev/null
+++ b/linebot/template_unmarshal.go
@@ -0,0 +1,173 @@
+package linebot
+
+import (
+	"encoding/json"
+	"errors"
+)
+
+// UnmarshalTemplateJSON function
+func UnmarshalTemplateJSON(data []byte) (Template, error) {
+	raw := rawTemplate{}
+	if err := json.Unmarshal(data, &raw); err != nil {
+		return nil, err
+	}
+	return raw.Template, nil
+}
+
+type rawTemplate struct {
+	Type     TemplateType `json:"type"`
+	Template Template     `json:"-"`
+}
+
+func (t *rawTemplate) UnmarshalJSON(data []byte) error {
+	type alias rawTemplate
+	raw := alias{}
+	if err := json.Unmarshal(data, &raw); err != nil {
+		return err
+	}
+	var template Template
+	switch raw.Type {
+	case TemplateTypeButtons:
+		template = &ButtonsTemplate{}
+	case TemplateTypeConfirm:
+		template = &ConfirmTemplate{}
+	case TemplateTypeCarousel:
+		template = &CarouselTemplate{}
+	case TemplateTypeImageCarousel:
+		template = &ImageCarouselTemplate{}
+	default:
+		return errors.New("invalid template type")
+	}
+	if err := json.Unmarshal(data, template); err != nil {
+		return err
+	}
+	t.Type = raw.Type
+	t.Template = template
+	return nil
+}
+
+// UnmarshalJSON method for ButtonsTemplate
+func (t *ButtonsTemplate) UnmarshalJSON(data []byte) error {
+	type alias ButtonsTemplate
+	raw := struct {
+		Actions []rawAction `json:"actions"`
+		*alias
+	}{
+		alias: (*alias)(t),
+	}
+	if err := json.Unmarshal(data, &raw); err != nil {
+		return err
+	}
+	actions := make([]TemplateAction, len(raw.Actions))
+	for i, action := range raw.Actions {
+		actions[i] = action.Action
+	}
+	t.Actions = actions
+	return nil
+}
+
+// UnmarshalJSON method for ConfirmTemplate
+func (t *ConfirmTemplate) UnmarshalJSON(data []byte) error {
+	type alias ConfirmTemplate
+	raw := struct {
+		Actions []rawAction `json:"actions"`
+		*alias
+	}{
+		alias: (*alias)(t),
+	}
+	if err := json.Unmarshal(data, &raw); err != nil {
+		return err
+	}
+	actions := make([]TemplateAction, len(raw.Actions))
+	for i, action := range raw.Actions {
+		actions[i] = action.Action
+	}
+	t.Actions = actions
+	return nil
+}
+
+type rawColumn struct {
+	Column CarouselColumn `json:"-"`
+}
+
+func (c *rawColumn) UnmarshalJSON(data []byte) error {
+	type alias CarouselColumn
+	raw := struct {
+		DefaultAction rawAction   `json:"defaultAction"`
+		Actions       []rawAction `json:"actions"`
+		*alias
+	}{
+		alias: (*alias)(&c.Column),
+	}
+	if err := json.Unmarshal(data, &raw); err != nil {
+		return err
+	}
+	c.Column.DefaultAction = raw.DefaultAction.Action
+	actions := make([]TemplateAction, len(raw.Actions))
+	for i, action := range raw.Actions {
+		actions[i] = action.Action
+	}
+	c.Column.Actions = actions
+	return nil
+}
+
+// UnmarshalJSON method for CarouselTemplate
+func (t *CarouselTemplate) UnmarshalJSON(data []byte) error {
+	type alias CarouselTemplate
+	raw := struct {
+		Columns []rawColumn `json:"columns"`
+		*alias
+	}{
+		alias: (*alias)(t),
+	}
+	if err := json.Unmarshal(data, &raw); err != nil {
+		return err
+	}
+	columns := make([]*CarouselColumn, len(raw.Columns))
+	for i, column := range raw.Columns {
+		c := column.Column
+		columns[i] = &c
+	}
+	t.Columns = columns
+	return nil
+}
+
+type rawImageColumn struct {
+	Column ImageCarouselColumn `json:"-"`
+}
+
+func (c *rawImageColumn) UnmarshalJSON(data []byte) error {
+	type alias ImageCarouselColumn
+	raw := struct {
+		Action rawAction `json:"action"`
+		*alias
+	}{
+		alias: (*alias)(&c.Column),
+	}
+	if err := json.Unmarshal(data, &raw); err != nil {
+		return err
+	}
+	c.Column.Action = raw.Action.Action
+	return nil
+}
+
+// UnmarshalJSON method for ImageCarouselTemplate
+func (t *ImageCarouselTemplate) UnmarshalJSON(data []byte) error {
+	type alias ImageCarouselTemplate
+	raw := struct {
+		Columns []rawImageColumn `json:"columns"`
+		*alias
+	}{
+		alias: (*alias)(t),
+	}
+	if err := json.Unmarshal(data, &raw); err != nil {
+		return err
+	}
+	columns := make([]*ImageCarouselColumn, len(raw.Columns))
+	for i, column := range raw.Columns {
+		c := column.Column
+		columns[i] = &c
+	}
+	t.Columns = columns
+	return nil
+}


Test patch:
diff --git a/linebot/template_test.go b/linebot/template_test.go
new file mode 100644
index 0000000..50ef720
--- /dev/null
+++ b/linebot/template_test.go
@@ -0,0 +1,357 @@
+package linebot
+
+import (
+	"reflect"
+	"strconv"
+	"testing"
+)
+
+func TestUnmarshalTemplateJSON(t *testing.T) {
+	testCases := []struct {
+		JSON []byte
+		Want Template
+	}{
+		{
+			JSON: []byte(`{
+	"type": "buttons",
+	"thumbnailImageUrl": "https://example.com/image.jpg",
+	"title": "Menu",
+	"text": "Please select",
+	"actions": [
+		{
+			"type": "postback",
+			"label": "postback",
+			"data": "action=buy&itemid=1",
+			"displayText": "postback text"
+		},
+		{
+			"type": "message",
+			"label": "message",
+			"text": "message text"
+		},
+		{
+			"type": "uri",
+			"label": "uri",
+			"uri": "http://example.com/",
+			"altUri": {
+				"desktop": "http://example.com/desktop"
+			}
+		}
+	]
+}`),
+			Want: &ButtonsTemplate{
+				ThumbnailImageURL: "https://example.com/image.jpg",
+				Title:             "Menu",
+				Text:              "Please select",
+				Actions: []TemplateAction{
+					&PostbackAction{
+						Label:       "postback",
+						DisplayText: "postback text",
+						Data:        "action=buy&itemid=1",
+					},
+					&MessageAction{
+						Label: "message",
+						Text:  "message text",
+					},
+					&URIAction{
+						Label: "uri",
+						URI:   "http://example.com/",
+						AltURI: &URIActionAltURI{
+							Desktop: "http://example.com/desktop",
+						},
+					},
+				},
+			},
+		},
+		{
+			JSON: []byte(`{
+	"type": "confirm",
+	"text": "Are you sure?",
+	"actions": [
+		{
+			"type": "message",
+			"label": "Yes",
+			"text": "yes"
+		},
+		{
+			"type": "message",
+			"label": "No",
+			"text": "no"
+		}
+	]
+}`),
+			Want: &ConfirmTemplate{
+				Text: "Are you sure?",
+				Actions: []TemplateAction{
+					&MessageAction{
+						Label: "Yes",
+						Text:  "yes",
+					},
+					&MessageAction{
+						Label: "No",
+						Text:  "no",
+					},
+				},
+			},
+		},
+		{
+			JSON: []byte(`{
+    "type": "carousel",
+    "columns": [
+    	{
+			"thumbnailImageUrl": "https://example.com/bot/images/item1.jpg",
+			"imageBackgroundColor": "#FFFFFF",
+			"title": "this is menu",
+			"text": "description",
+			"defaultAction": {
+			"type": "uri",
+			"label": "View detail",
+			"uri": "http://example.com/page/123"
+			},
+			"actions": [
+			{
+				"type": "postback",
+				"label": "Buy",
+				"data": "action=buy&itemid=111"
+			},
+			{
+				"type": "postback",
+				"label": "Add to cart",
+				"data": "action=add&itemid=111"
+			},
+			{
+				"type": "uri",
+				"label": "View detail",
+				"uri": "http://example.com/page/111"
+			}
+			]
+		},
+		{
+			"thumbnailImageUrl": "https://example.com/bot/images/item2.jpg",
+			"imageBackgroundColor": "#000000",
+			"title": "this is menu",
+			"text": "description",
+			"defaultAction": {
+			"type": "uri",
+			"label": "View detail",
+			"uri": "http://example.com/page/222"
+			},
+			"actions": [
+			{
+				"type": "postback",
+				"label": "Buy",
+				"data": "action=buy&itemid=222"
+			},
+			{
+				"type": "postback",
+				"label": "Add to cart",
+				"data": "action=add&itemid=222"
+			},
+			{
+				"type": "uri",
+				"label": "View detail",
+				"uri": "http://example.com/page/222"
+			}
+			]
+		}
+		],
+		"imageAspectRatio": "rectangle",
+		"imageSize": "cover"
+}`),
+			Want: &CarouselTemplate{
+				Columns: []*CarouselColumn{
+					{
+						ThumbnailImageURL:    "https://example.com/bot/images/item1.jpg",
+						ImageBackgroundColor: "#FFFFFF",
+						Title:                "this is menu",
+						Text:                 "description",
+						DefaultAction: &URIAction{
+							Label: "View detail",
+							URI:   "http://example.com/page/123",
+						},
+						Actions: []TemplateAction{
+							&PostbackAction{
+								Label: "Buy",
+								Data:  "action=buy&itemid=111",
+							},
+							&PostbackAction{
+								Label: "Add to cart",
+								Data:  "action=add&itemid=111",
+							},
+							&URIAction{
+								Label: "View detail",
+								URI:   "http://example.com/page/111",
+							},
+						},
+					},
+					{
+						ThumbnailImageURL:    "https://example.com/bot/images/item2.jpg",
+						ImageBackgroundColor: "#000000",
+						Title:                "this is menu",
+						Text:                 "description",
+						DefaultAction: &URIAction{
+							Label: "View detail",
+							URI:   "http://example.com/page/222",
+						},
+						Actions: []TemplateAction{
+							&PostbackAction{
+								Label: "Buy",
+								Data:  "action=buy&itemid=222",
+							},
+							&PostbackAction{
+								Label: "Add to cart",
+								Data:  "action=add&itemid=222",
+							},
+							&URIAction{
+								Label: "View detail",
+								URI:   "http://example.com/page/222",
+							},
+						},
+					},
+				},
+				ImageAspectRatio: "rectangle",
+				ImageSize:        "cover",
+			},
+		},
+		{
+			JSON: []byte(`{
+	"type": "image_carousel",
+	"columns": [
+	{
+		"imageUrl": "https://example.com/bot/images/item1.jpg",
+		"action": {
+		"type": "postback",
+		"label": "Buy",
+		"data": "action=buy&itemid=111"
+		}
+	},
+	{
+		"imageUrl": "https://example.com/bot/images/item2.jpg",
+		"action": {
+		"type": "message",
+		"label": "Yes",
+		"text": "yes"
+		}
+	},
+	{
+		"imageUrl": "https://example.com/bot/images/item3.jpg",
+		"action": {
+		"type": "uri",
+		"label": "View detail",
+		"uri": "http://example.com/page/222"
+		}
+	}
+	]
+}`),
+			Want: &ImageCarouselTemplate{
+				Columns: []*ImageCarouselColumn{
+					{
+						ImageURL: "https://example.com/bot/images/item1.jpg",
+						Action: &PostbackAction{
+							Label: "Buy",
+							Data:  "action=buy&itemid=111",
+						},
+					},
+					{
+						ImageURL: "https://example.com/bot/images/item2.jpg",
+						Action: &MessageAction{
+							Label: "Yes",
+							Text:  "yes",
+						},
+					},
+					{
+						ImageURL: "https://example.com/bot/images/item3.jpg",
+						Action: &URIAction{
+							Label: "View detail",
+							URI:   "http://example.com/page/222",
+						},
+					},
+				},
+			},
+		},
+	}
+	for i, tc := range testCases {
+		t.Run(strconv.Itoa(i), func(t *testing.T) {
+			template, err := UnmarshalTemplateJSON(tc.JSON)
+			if err != nil {
+				t.Fatal(err)
+			}
+			if !reflect.DeepEqual(template, tc.Want) {
+				t.Errorf("got %v, want %v", template, tc.Want)
+			}
+		})
+	}
+}
+
+func BenchmarkUnmarshalTemplateJSON(b *testing.B) {
+	var jsonData = []byte(`{
+    "type": "carousel",
+    "columns": [
+    	{
+			"thumbnailImageUrl": "https://example.com/bot/images/item1.jpg",
+			"imageBackgroundColor": "#FFFFFF",
+			"title": "this is menu",
+			"text": "description",
+			"defaultAction": {
+			"type": "uri",
+			"label": "View detail",
+			"uri": "http://example.com/page/123"
+			},
+			"actions": [
+			{
+				"type": "postback",
+				"label": "Buy",
+				"data": "action=buy&itemid=111"
+			},
+			{
+				"type": "postback",
+				"label": "Add to cart",
+				"data": "action=add&itemid=111"
+			},
+			{
+				"type": "uri",
+				"label": "View detail",
+				"uri": "http://example.com/page/111"
+			}
+			]
+		},
+		{
+			"thumbnailImageUrl": "https://example.com/bot/images/item2.jpg",
+			"imageBackgroundColor": "#000000",
+			"title": "this is menu",
+			"text": "description",
+			"defaultAction": {
+			"type": "uri",
+			"label": "View detail",
+			"uri": "http://example.com/page/222"
+			},
+			"actions": [
+			{
+				"type": "postback",
+				"label": "Buy",
+				"data": "action=buy&itemid=222"
+			},
+			{
+				"type": "postback",
+				"label": "Add to cart",
+				"data": "action=add&itemid=222"
+			},
+			{
+				"type": "uri",
+				"label": "View detail",
+				"uri": "http://example.com/page/222"
+			}
+			]
+		}
+		],
+		"imageAspectRatio": "rectangle",
+		"imageSize": "cover"
+}`)
+	b.ResetTimer()
+	for i := 0; i < b.N; i++ {
+		_, err := UnmarshalTemplateJSON(jsonData)
+		if err != nil {
+			b.Fatal(err)
+		}
+	}
+}