この記事はLINE Engineering Blog 「夏休みの自由研究 -Summer Homework-」の10日目の記事です。
京都開発室の @sugyan です。京都の夏を初体験していますが、いやー凄まじい暑さですね。
さて、2018年6月、LINE Messaging APIで利用できるMessageタイプとして「Flex Message」が新たに加わり、複雑なレイアウトも可能なカスタマイズ性の高いメッセージを送信することができるようになりました。
公式SDKでも早速Flex Messageをサポートをしていこう、と実装を始めました。
今回はGo SDK担当としてFlex Messageのための機能実装で得た知見などを書いていこうと思います。
Go SDKでのFlex Messageの使い方
送信されてきたメッセージに対して Flex Message を使った返信を行う場合、以下のようなコードになります。
import "github.com/line/line-bot-sdk-go/linebot"
func reply(replyToken string) error {
container := &linebot.BubbleContainer{
Type: linebot.FlexContainerTypeBubble,
Body: &linebot.BoxComponent{
Type: linebot.FlexComponentTypeBox,
Layout: linebot.FlexBoxLayoutTypeHorizontal,
Contents: []linebot.FlexComponent{
&linebot.TextComponent{
Type: linebot.FlexComponentTypeText,
Text: "Hello,",
},
&linebot.TextComponent{
Type: linebot.FlexComponentTypeText,
Text: "World!",
},
},
},
}
if _, err := client.ReplyMessage(
replyToken,
linebot.NewFlexMessage("alt text", container),
).Do(); err != nil {
return err
}
return nil
}
*linebot.Client
の ReplyMessage(replyToken string, messages ...Message)
メソッドには Message
interface を実装したオジェクトを渡すようになっています。
Flex Message を送信する場合は func NewFlexMessage(altText string, container FlexContainer)
関数で作成される *FlexMessage
が Message
interface を実装したオブジェクトとなります。
NewFlexMessage()
関数に渡す2つ目の引数は同様に FlexContainer
interface を実装したオブジェクトを渡すようになっており、ここでは *BubbleContainer
もしくは *CarouselContainer
がそれを実装していますので、どちらかを渡すことになります。上記例では *BubbleContainer
を使っていますね。
いちいちすべての struct
を記述するのも面倒なので、所謂Builderを利用する仕組みを用意してみようかとも考えましたが、結局メリットは Type
要素を省略できる程度であまり便利になりそうにもなく、SDKのメンテナンスの負担が増えるだけだと判断し、敢えて採用しませんでした。
その代わりに、より実用的に使用できる方法として、JSON文字列から FlexMessage
オブジェクトを生成できる関数 func UnmarshalFlexMessageJSON([]byte)
を用意しました。
Simulatorから生成されたJSONを利用する
実際には FlexMessage は特定のJSONデータをリクエストのbodyに含めてPOSTするようになっています。そのJSONと描画結果を確認するためのツールとしてSimulatorが提供されています。
例えば "Restaurant" というサンプルメッセージから「作成する」を選択すると、以下のようなものが生成されます。左に描画結果が表示されていて、右側のJSONを編集して「プレビュー」ボタンを押すと変更が描画結果に反映されます。
このSimulatorで正しく描画できたJSONは、そのままリクエストのbodyでPOSTできるvalidなデータとなります。SDKとしてもこういったJSONデータをそのまま FlexMessage として送信できるようにしたいところですが、それはつまり []byte
(もしくは string
) を引数で渡せるようにするしかなく、それではデタラメでinvalidなJSONデータ(そもそもJSONとして正しくないものですら)渡せるようになってしまいます。
なので、SDKでは JSONデータを受け取り FlexContainer
interface を実装したオブジェクトに変換する UnmarshalFlexMessageJSON([]byte)
関数を用意しました。
これを利用することで、プログラムは正しいJSONデータだけを受け取り、前述の例と同じ仕組みで FlexMessage を送信できるようになります。
jsonData := []byte(`{
"type": "bubble",
...
}`)
container, err := linebot.UnmarshalFlexMessageJSON(jsonData)
if err != nil {
// 正しくUnmarshalできないinvalidなJSONであればerrが返る
...
}
message := linebot.NewFlexMessage("alt text", container)
ひと手間かかってはしまいますが、Simulatorで確認したJSONをそのままコピーして利用することができます。
UnmarshalJSON するために
さて、その UnmarshalFlexMessageJSON([]byte)
の実装です。
基本的には json.Unmarshal
を使ってJSONデータを Unmarshal してそれぞれの型にマッピングしていくのですが、それほど単純ではありません。
何故ならマッピングすべき型が決まっていないからです。まず FlexContainer
interface は上述したように *BubbleContainer
もしくは *CarouselContainer
のどちらかのオブジェクトですし、その中に含まれる header
, body
, footer
の *BoxComponent
要素がそれぞれ持つ contents
要素はまた *BoxComponent
や *ButtonComponent
など、 FlexComponent
interface として定義したものを実装したオブジェクトの配列となります。
また さらに、幾つかの FlexComponent
が含む action
要素は Template Message などでも利用しているオブジェクトで、これも TemplateAction
interface を実装している複数のタイプのものが存在します。
ただし、JSON内の各オブジェクトにはそれぞれ必ず type
要素が含まれており、どの型のデータであるかは示されています。
{
...
"contents": [
{
"type": "icon",
"size": "sm",
"url": "https://scdn.line-apps.com/n/channel_devcenter/img/fx/review_gray_star_28.png"
},
{
"type": "text",
"text": "4.0",
"size": "sm",
"color": "#999999",
"margin": "md",
"flex": 0
}
],
...
}
type
要素を見れば、contents
の内容の1つ目は size
や url
を含む *IconComponent
型、2つ目は color
や margin
などを含む *TextComponent
型にUnmarshalしていけば良い、ということが分かるわけようになっています。
Unmarshal用の type を使う
とはいえ json.Unmarshal
は先にUnmarshalする先の型が決まっていないと呼び出すことができません。
しかし type
の中身を読まないとUnmarshalすべき型が分からないので、呼び出す前にそれを定義することが出来ません。
ですので、SDKではUnmarshal用の別typeを用意し、その中でまずは type
だけ先に読み取り、それから改めて型を決定してUnmarshalする、という方法を取りました。
例えば、 *FlexContainer
オブジェクトに変換する部分のコードは以下のような実装になっています。
func UnmarshalFlexMessageJSON(data []byte) (FlexContainer, error) {
raw := rawFlexContainer{}
if err := json.Unmarshal(data, &raw); err != nil {
return nil, err
}
return raw.Container, nil
}
type rawFlexContainer struct {
Type FlexContainerType `json:"type"`
Container FlexContainer `json:"-"`
}
func (c *rawFlexContainer) UnmarshalJSON(data []byte) error {
type alias rawFlexContainer
raw := alias{}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
var container FlexContainer
switch raw.Type {
case FlexContainerTypeBubble:
container = &BubbleContainer{}
case FlexContainerTypeCarousel:
container = &CarouselContainer{}
default:
return errors.New("invalid container type")
}
if err := json.Unmarshal(data, container); err != nil {
return err
}
c.Type = raw.Type
c.Container = container
return nil
}
Unmarshalだけのために使う rawFlexContainer
という型がポイントです。
これは UnmarshalJSON([]byte)
を実装しているので、 json.Unmarshal
関数でこの型のオブジェクトを引数に指定されていると、この UnmarshalJSON
が呼ばれることになります。
alias type
この func (c *rawFlexContainer) UnmarshalJSON([]byte)
中の1行目で type alias rawFlexContainer
として「同じ内容ではあるが別名の型」を定義します。
この alias
型は rawFlexContainer
とは異なるので json.Unmarshal
関数の引数で指定されても (*rawFlexContainer) UnmarshalJSON
を呼ばれることはありません。
`json`
tag を見ていただくと分かる通り、この alias
型のオブジェクトへの json.Unmarshal
は type
要素しかUnmarshalしません。
しかし、それによってマッピングすべき型を判別できるので、その type
の値に応じた型のオブジェクトを用意し、改めて 渡されてきたデータを目的の型へ json.Unmarshal
で変換することができます。
こうして 内部的には渡されたデータを 2回 json.Unmarshal
にかける形になるのですが、正しくマッピングすべき型を取得しながらUnmarshalしていくことができるわけですね。
FlexComponent
interfaceのオブジェクトへのUnmarshalも同様に、 rawFlexComponent
という型を用意し、 alias type を利用してマッピングすべき型のオブジェクトを用意して処理します。
type rawFlexComponent struct {
Type FlexComponentType `json:"type"`
Component FlexComponent `json:"-"`
}
func (c *rawFlexComponent) UnmarshalJSON(data []byte) error {
type alias rawFlexComponent
raw := alias{}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
var component FlexComponent
switch raw.Type {
case FlexComponentTypeBox:
component = &BoxComponent{}
case FlexComponentTypeButton:
component = &ButtonComponent{}
case FlexComponentTypeFiller:
component = &FillerComponent{}
case FlexComponentTypeIcon:
component = &IconComponent{}
case FlexComponentTypeImage:
component = &ImageComponent{}
case FlexComponentTypeSeparator:
component = &SeparatorComponent{}
case FlexComponentTypeSpacer:
component = &SpacerComponent{}
case FlexComponentTypeText:
component = &TextComponent{}
default:
return errors.New("invalid flex component type")
}
if err := json.Unmarshal(data, component); err != nil {
return err
}
c.Type = raw.Type
c.Component = component
return nil
}
これを利用し、Contents []FlexComponent
要素を持つ *BoxComponent
をUnmarshalする処理は以下のようになります。
type rawFlexComponent struct {
Type FlexComponentType `json:"type"`
Component FlexComponent `json:"-"`
}
func (c *BoxComponent) UnmarshalJSON(data []byte) error {
type alias BoxComponent
raw := struct {
Contents []rawFlexComponent `json:"contents"`
*alias
}{
alias: (*alias)(c),
}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
components := make([]FlexComponent, len(raw.Contents))
for i, content := range raw.Contents {
components[i] = content.Component
}
c.Contents = components
return nil
}
alias
からさらに Contents
の部分だけ上書きした別のstructを利用することで、Contents
要素以外の値をそのまま呼び出し元の *BoxComponent
の値としてUnmarshalできます。Contents
要素だけは上書きされて rawFlexComponent
のスライスとしてUnmarshalされているので、それぞれその中身のComponentを取り出すことで、すべてのUnmarshal済みのものを取得することができます。
reflectを使用する場合
余談ですが、得られた type
要素から switch
文を使ってマッピングすべき型のオブジェクトを定義して、というところはちょっとカッコ悪い感じがしますね。
reflect
を使うと、あらかじめ作るべきオブジェクトの型を値として用意しておき、その型のオブジェクトを動的に作って利用することができます。
例えば rawFlexContainer
の場合、型名と reflect.Type
のマッピングを用意して、以下のように書き換えることが可能です。
import "reflect"
var containerTypeMap = map[FlexContainerType]reflect.Type{
FlexContainerTypeBubble: reflect.TypeOf(BubbleContainer{}),
FlexContainerTypeCarousel: reflect.TypeOf(CarouselContainer{}),
}
func (c *rawFlexContainer) UnmarshalJSON(data []byte) error {
type alias rawFlexContainer
raw := alias{}
if err := json.Unmarshal(data, &raw); err != nil {
return err
}
container := reflect.New(containerTypeMap[raw.Type]).Interface()
if err := json.Unmarshal(data, container); err != nil {
return err
}
if flexContainer, ok := container.(FlexContainer); ok {
c.Container = flexContainer
}
return nil
}
switch
文が消えて幾分スッキリしますが、今回SDKではこの方法は敢えて採用しませんでした。reflect
は便利ですが使い方を誤ると黒魔術的なものになりやすく、メンテナンス性も下がる可能性があるからです。reflect
を使わずに素朴な switch
文などで目的の動作を達成できるならわざわざ導入する必要はなく、どうしても refrect
を使わないと実現できない機能があるという場面でもない限りは基本的に使わないようにしよう、という判断です。
まとめ
Messaging APIのGo SDKでのFlex Messageの実装について紹介しました。
型付き言語であるGoで 様々な型の可能性を持つJSONデータを扱うのは多少大変ですが、少し工夫すれば問題なく実現できることが示せたかと思います。
(本記事は2018年8月現在でのものです。より良い方法などがありましたら皆様からのPull-Requestをお待ちしています。)
Special thanks to @mokejp!
明日は Lee Jihoon による「Best practices to secure your SSL/TLS Implementation」です。お楽しみに!