【AWS】API GatewayのMapping Templateで、Key=ValueペアをJSONに変換する

# はじめに
API Gatewayでapplication/x-www-form-urlencoded形式のPOSTリクエストを受け取るための方法を説明します。
これは、HTTPのPOSTリクエストのBodyにGETパラメータのような形式の文字列（Key1=Value1&Key2=Value2...）が指定される形式となります。このようなリクエストをapplication/json形式に変換するためのTemplateが必要となったのでまとめました。

なお、下記の記事からの派生となります。TwilioからのPOSTリクエストをJSONに変換するために必要でした。
[仕組みから理解するTwilio #3-1 - AWS API Gateway+Lambda実装Walkthrough(前編)](http://qiita.com/tmiki/items/7f55a81c46dd07b2e4fa)

# Mapping Templateとは
API Gatewayで、データ形式を変換するための仕組み。Velocity Template Language(以下、VTL)とJSONPathを利用して定義します。

Amazon API Gateway API リクエストとレスポンスペイロードのマッピングを設定する
https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/models-mappings.html

Apache Velocity Engine - User Guide
http://velocity.apache.org/engine/devel/user-guide.html

Apache Velocity Engine VTL Reference
http://velocity.apache.org/engine/devel/vtl-reference.html

JSONPath - XPath for JSON
http://goessner.net/articles/JsonPath/


# やりたいこと
application/x-www-form-urlencoded形式のPOSTリクエストをJSONに変換することが目的です。
POSTメソッドで送信されたHTTPリクエストBodyに「&」で区切られたKey=Valueペア（改行なし）が設定されるので、これをJSONに変換することとなります。
Valueが設定されていないKeyは無視します。

```:Input
ApiVersion=2010-04-01&ApplicationSid=AP75zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz&Called=&Caller=client%3ADeviceId_0001&From=client%3ADeviceId_0001&To=&callerPhoneNumber=%2B81-50-3123-4567&callOutgoingPhoneNumber=%2B81-90-5987-6543
```

```json:Output
{
    "From": "client:DeviceId_0001",
    "Caller": "client:DeviceId_0001",
    "ApiVersion": "2010-04-01",
    "ApplicationSid": "AP75zzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
    "callerPhoneNumber": "+81-50-3123-4567",
    "callOutgoingPhoneNumber": "+81-90-5987-6543"
}
```

「Called」はKeyのみでValueが存在しないため、変換後のJSONには存在していません。


# Templateの実装
## Twilio特化型

```
#set($raw_input = $input.path("$"))

{
  #foreach( $kvpairs in $raw_input.split( '&' ) )
    #set( $kvpair = $kvpairs.split( '=' ) )
      #if($kvpair.length == 2)
        "$kvpair[0]" : "$kvpair[1]"#if( $foreach.hasNext ),#end
      #end
  #end
}
```

リクエストBodyに設定された文字列を「&」で分割したのち、それらを更に「=」で分割してKeyとValueの値を取り出し、JSONオブジェクトのペアとして出力する、ということを行っています。
ポイントは、Key=Valueを「=」で分割した後に、要素数をチェックしているところです。要素数が2の場合のみ、変換後のJSONに出力します。それ以外の要素数に分割された場合は不正とみなし無視します。


## 汎用型
GETクエリパラメータにも対応した汎用型のテンプレートは下記になります。AWS Developer Forumで議論され、まとめてられていた内容に若干手を加えたものです。
下記の記事で最終的にまとめられていたものは、Keyの指定があるがValueが設定されていないケース(「Key2=&Key3=Value3」のKey2ようなケース)に対応していなかったので修正しました。

HOWTO: Mapping Template v3.0 to convert form POST data or GET query to JSON
https://forums.aws.amazon.com/thread.jspa?messageID=673012&tstart=0#725034

```
## 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.size() == 2) && ($kvTokenised[0].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
}
```