application/x-www-form-urlencodedのマッピングテンプレートを記述する(AWS API Gateway)

TIPS

こんにちは。ざわかける!のざわ(@zw_kakeru)です。
今回はAWS API Gatewayで使用するapplication/x-www-form-urlencoded形式のテンプレートについて、変換スクリプトの文法と書き方を調べて実際に書いてみます。

やりたいこと

SlackAPI ⇄ AWS API Gateway ⇄ AWS Lambda の実装を行う際、SlackAPIから送られてくるリクエスト形式をAPI Gateway上で変換してからLambdaに渡す必要が出てきました。
すなわちSlackAPIから送られてくる

token=gIkuvaNzQIHg97ATvDxqgjtO
&team_id=T0001
&team_domain=example
&enterprise_id=E0001
&enterprise_name=Globular%20Construct%20Inc
&channel_id=C2147483705
&channel_name=test
&user_id=U2147483697
&user_name=Steve
&command=/weather
&text=94070
&response_url=https://hooks.slack.com/commands/1234/5678
&trigger_id=13345224609.738474920.8088930838d88f008e0
&api_app_id=A123456

このURLパラメータ形式のデータ(AWS上ではapplication/x-www-form-urlencoded形式として扱われます)を

{
  "token": "gIkuvaNzQIHg97ATvDxqgjtO",
  "team_id": "T0001",
  "team_domain": "example",
  "enterprise_id": "E0001",
  "enterprise_name": "Globular%20Construct%20Inc",
  "channel_id": "C2147483705",
  "channel_name": "test",
  "user_id": "U2147483697",
  "user_name": "Steve",
  "command": "/weather",
  "text": "94070",
  "response_url": "https://hooks.slack.com/commands/1234/5678",
  "trigger_id": "13345224609.738474920.8088930838d88f008e0",
  "api_app_id": "A123456"
}

こうJSON形式に変換しなければいけません。
私の場合はtokenとtextだけ取得できれば良かったので、さらに絞って

{
  "token": "gIkuvaNzQIHg97ATvDxqgjtO",
  "text": "94070",
}

こうなるようにスクリプトを書いていきます。

VTL(Velocity Template Language)での記述

このマッピングテンプレートで使われているのはVTL(Velocity Template Language)という言語です。
言語仕様はこちらです。

このVTLはドメイン固有言語(DSL)の一種であり、このマッピングテンプレートを書くためだけに存在している言語です(厳密には違いますが)。
そのため書き方も特殊になっている代わりに実行時のオーバーヘッドが無く、高パフォーマンスでの内部動作が実現できます。
とはいえここで扱うVTLの記述はそこまで特殊なものではなく、プログラミングができる人なら普通に読めば理解できますし書くこともできます。
言語仕様も簡単なのでサクッと読んで書いてみるのをおすすめします。

やったこと

本題に戻ります。
クエリ文字列型式で送られてきたパラメータをJSONに変換する記述は次のとおりです。

## 全パラメータを取得
#set($rowApiData = $input.path("$"))

## &で分割
#set($apiParams = $rowApiData.split("&"))

## result用配列の初期化
#set($resultParams = [])

## token, text行のみ抽出
#foreach( $kvRaw in $apiParams )
  #set($keyValue = $kvRaw.split("="))
  #if ( ( $keyValue[0] == "token" ) || ( $keyValue[0] == "text" ) )
    #set($_ = $resultParams.add($kvRaw))
  #end
#end

## json形式に変換
{
  #foreach( $kvRaw in $resultParams )
    #set($keyValue = $kvRaw.split("="))
    "$util.urlDecode($keyValue[0])" : "$util.urlDecode($keyValue[1])" #if( $foreach.hasNext ),#end
  #end
}

以上のコードをマッピングテンプレートのコード欄に記載すれば、送られてきたクエリ文字列をパースしてtextとtokenだけをJSONに変換することが可能です。
コメントも記載しておいたので何をやっているかは分かるかと思います。
ポイントとしてはVTLは #foreach や #set など、各種命令は#をつける仕様になっています。
##と#が二つ続くとコメント行と見なされます。
$util.urlDecodeについてはAWS側の記述方法で、公式ドキュメントに詳細が書かれています。

ちなみに、記述の元としたのはこちらのQiitaの記事で、この記事のコードを使えばtextとtokenだけでなく全てのURLパラメータをJSONに変換することができます。
一応ここにもそのコードを載せておきます。

## convert HTML POST data or HTTP GET query string to JSON

## get the raw post data from the AWS built-in variable and give it a nicer name
#if ($context.httpMethod == "POST")
 #set($rawAPIData = $input.path("$"))
#elseif ($context.httpMethod == "GET")
 #set($rawAPIData = $input.params().querystring)
 #set($rawAPIData = $rawAPIData.toString())
 #set($rawAPIDataLength = $rawAPIData.length() - 1)
 #set($rawAPIData = $rawAPIData.substring(1, $rawAPIDataLength))
 #set($rawAPIData = $rawAPIData.replace(", ", "&"))
#else
 #set($rawAPIData = "")
#end

## first we get the number of "&" in the string, this tells us if there is more than one key value pair
#set($countAmpersands = $rawAPIData.length() - $rawAPIData.replace("&", "").length())

## if there are no "&" at all then we have only one key value pair.
## we append an ampersand to the string so that we can tokenise it the same way as multiple kv pairs.
## the "empty" kv pair to the right of the ampersand will be ignored anyway.
#if ($countAmpersands == 0)
 #set($rawPostData = $rawAPIData + "&")
#end

## now we tokenise using the ampersand(s)
#set($tokenisedAmpersand = $rawAPIData.split("&"))

## we set up a variable to hold the valid key value pairs
#set($tokenisedEquals = [])

## now we set up a loop to find the valid key value pairs, which must contain only one "="
#foreach( $kvPair in $tokenisedAmpersand )
 #set($countEquals = $kvPair.length() - $kvPair.replace("=", "").length())
 #if ($countEquals == 1)
  #set($kvTokenised = $kvPair.split("="))
  #if ( ( $kvTokenised[0].length() > 0 ) && ( $kvTokenised[1].length() > 0 ) )
   ## we found a valid key value pair. add it to the list.
   #set($devNull = $tokenisedEquals.add($kvPair))
  #end
 #end
#end

## next we set up our loop inside the output structure "{" and "}"
{
#foreach( $kvPair in $tokenisedEquals )
  ## finally we output the JSON for this pair and append a comma if this isn't the last pair
  #set($kvTokenised = $kvPair.split("="))
 "$util.urlDecode($kvTokenised[0])" : #if($kvTokenised.size() > 1 && $kvTokenised[1].length() > 0)"$util.urlDecode($kvTokenised[1])"#{else}""#end#if( $foreach.hasNext ),#end
#end
}

おわりに

ドメイン固有言語は初めて出会った時に「何だこの言語は!?」とびっくりしてしまいますが、よく見ると普通に理解できるので恐れずに立ち向かっていきましょう。
しかしながら、その辺の説明をもう少し分かりやすく書いてくれてもいいのになあ…とも思いますね。
その辺りも慣れていきつつ、自分が実現したいことを正しく記述していってください。

タイトルとURLをコピーしました