API GatewayのVTLテンプレートだけでタイムゾーンAsia/TokyoでISO8601形式の文字列を生成する曲芸に挑戦してみた

API GatewayのVTLテンプレートだけでタイムゾーンAsia/TokyoでISO8601形式の文字列を生成する曲芸に挑戦してみた

Clock Icon2024.07.21

リテールアプリ共創部@大阪の岩田です

API GatewayのAWS統合を使うとLambdaを用いなくてもAPI GatewayとAWSのサービスを簡単に統合できます。さらにVTLテンプレートを活用することで、動的なパラメータの設定などある程度柔軟に制御することができます。

とある趣味プロジェクトでAPI GatewayのAWS統合を使ってDynamoDBのPutItemを呼び出す際にISO8601形式の日付文字列を動的に生成する必要が出てきたのですが、かなり実装に苦戦したので最終的なVTLテンプレートの中身や注意点などここに供養したいと思います。

VTLテンプレート

最終的なVTLテンプレートは以下の通りです。以下サイトで紹介されているコードをVTLテンプレートに書き換えた形になります。

【寄り道】UNIXTIMEを普通の日付に変換するためのプログラム - その漫画自炊オタクはImageJマクロに恋をする

ロジックの中身などは上記サイトがさらに参考にしている以下のサイトで解説されてたので、興味がある方はそちらを参照して下さい。

UNIXTIME

#set($dummyIntClass = 0)
#set($SECONDS_IN_A_DAY = 24 * 60 * 60)
#set($UNIX_EPOCH_DAY = 1969 * 365 + 492 - 19 + 4 + 306)
#set($YEAR_ONE = 365)
#set($YEAR_FOUR = $YEAR_ONE * 4 + 1)
#set($YEAR_100 = $YEAR_FOUR * 25 - 1)
#set($YEAR_400 = $YEAR_100 * 4 + 1)
#set($monthday = [0, 31, 61, 92, 122, 153, 184, 214, 245, 275, 306, 337])
#set($GMT_TOKYO = 9 * 60 * 60)
#set($unixtime = ($context.requestTimeEpoch / 1000))
#set($unixtime = $unixtime + $GMT_TOKYO)
#set($second = ($unixtime % 60))
#set($minute = ($unixtime / 60 % 60))
#set($hour = ($unixtime / 3600 % 24))
#set($leap = 0)
#set($unixday = ($unixtime / $SECONDS_IN_A_DAY))
#set($splitted = $unixday.toString().split("\."))
#set($unixday = $dummyIntClass.parseInt($splitted[0]))
#set($unixday = $unixday + $UNIX_EPOCH_DAY)
#set($tmp = ($unixday / $YEAR_400))
#set($tmp = $dummyIntClass.parseInt($tmp.toString().split("\.")[0]))
#set($year = 400 * $tmp)
#set($unixday = $unixday % $YEAR_400)
#set($tmp = $unixday / $YEAR_100)
#set($n = $dummyIntClass.parseInt($tmp.toString().split("\.")[0]))
#set($year = $year + $n * 100)
#set($unixday = $unixday % $YEAR_100)
#if($n == 4)
  #set($leap = 1)
#else
  #set($tmp = $unixday / $YEAR_FOUR)
  #set($tmp = $dummyIntClass.parseInt($tmp.toString().split("\.")[0]))
  #set($year = $year + 4 * $tmp)
  #set($unixday = $unixday % $YEAR_FOUR)
  #set($tmp = $unixday / $YEAR_ONE)
  #set($tmp = $dummyIntClass.parseInt($tmp.toString().split("\.")[0]))
  #set($n = $tmp)
  #set($year = $year + $n)
  #set($unixday = $unixday % $YEAR_ONE)
  #if($n == 4)
    #set($leap = 1)
  #end
#end
#if($leap != 0)
  #set($month = 2)
  #set($day = 29)
#else
  #set($tmp = ($unixday + 0.4) * 5 / 153)
  #set($tmp = $dummyIntClass.parseInt($tmp.toString().split("\.")[0]))
  #set($month = $tmp)
  #set($day = $unixday - $monthday.get($month) + 1)
  #set($month = $month + 3)
  #if($month > 12)
    #set($year = $year + 1)
    #set($month = $month - 12)
  #end
#end
#if($month.toString().length() == 1)
  #set($month = "0" + $month.toString())
#else
  #set($month = $month.toString())
#end
#if($day.toString().length() == 1)
  #set($day = "0" + $day.toString())
#else
  #set($day = $day.toString())
#end
#if($hour.toString().length() == 1)
  #set($hour = "0" + $hour.toString())
#else
  #set($hour = $hour.toString())
#end
#if($minute.toString().length() == 1)
  #set($minute = "0" + $minute.toString())
#else
  #set($minute = $minute.toString())
#end
#if($second.toString().length() == 1)
  #set($second = "0" + $second.toString())
#else
  #set($second = $second.toString())
#end
#set($isoDate = $year.toString() + "-" + $month + "-" + $day + "T" + $hour + ":" + $minute.toString() + ":" + $second.toString() + "+09:00")
{
"date": "$isoDate"
}

苦労した点

ここからは色々と苦労/工夫した点について紹介したいと思います。

App SyncのVTLテンプレートで実現できる処理がAPI Gatewayだと未対応

App SyncのVTLテンプレートだと$utilを使うことで日時関連の処理も簡単に実現可能です。

App Syncのドキュメントでは例として以下のような実装が紹介されています。

$util.time.nowISO8601()                                            : 2018-02-06T19:01:35.749Z
$util.time.nowEpochSeconds()                                       : 1517943695
$util.time.nowEpochMilliSeconds()                                  : 1517943695750
$util.time.nowFormatted("yyyy-MM-dd HH:mm:ssZ")                    : 2018-02-06 19:01:35+0000
$util.time.nowFormatted("yyyy-MM-dd HH:mm:ssZ", "+08:00")          : 2018-02-07 03:01:35+0800
$util.time.nowFormatted("yyyy-MM-dd HH:mm:ssZ", "Australia/Perth") : 2018-02-07 03:01:35+0800

$util.time の日時ヘルパー - AWS AppSync

しかしAPI GatewayのVTLだと$utilで利用可能な処理は以下の6つだけです。

  • $util.escapeJavaScript()
  • $util.parseJson()
  • $util.urlEncode()
  • $util.urlDecode()
  • $util.base64Encode()
  • $util.base64Decode()

日付/日時に関連する便利な処理は提供されていないので、全て自前で実装する必要があります。

タイムゾーンをAsia/Tokyoにしたい

ISO8601形式の文字列を生成するだけであれば以下Stack Overflowの記事に実装例が見つかりました。

amazon web services - How can I create a current ISO8601 timestamp in AWS Api Gateway Mapping Template (Velocity, VTL)? - Stack Overflow

内容的にも理解しやすくて良かったのですが、この実装例だとUTCでの日付文字列になってしまいます。タイムゾーンをAsia/Tokyoにするには$context.requestTimeEpochで取得したユニックスタイムスタンプをISO8601形式に変換する必要がありました。

マクロが使えなかった

VTLテンプレート内で何度も繰り返す処理を共通化したかったのですが、#macroは使えないようでした。ということで冗長ですが何度も同じような処理を記述する必要があります。

Apache Velocity Engine - User Guide

文字列を数値にキャストするために一手間必要

VTLだと$convertを使って型変換が可能なようですが、API GatewayのVTLでは$convertが使えませんでした。

Apache Velocity Tools Usage Summary

ということで、#set($dummyIntClass = 0) で宣言したダミーの変数を使って$dummyIntClass.parseInt("1")のようにparseIntを呼び出すことで文字列から数値へのキャストを実現しました。

math.floor的な処理が使えない

API GatewayのVTLでは$mathも使えませんでした。

Apache Velocity Tools Usage Summary

ということで以下のような形で切り捨て処理も自前で実装しました。

#set($tmp = $unixday / $YEAR_FOUR)
#set($tmp = $dummyIntClass.parseInt($tmp.toString().split("\.")[0]))

割り算に実行結果を文字列にキャストした後に.で分割してから1要素目を取得し、また数値にキャストすることで切り捨て処理を実現しています。

0詰めが面倒

日や時間を2桁に0詰めする簡単な処理が無さそうだったので、以下のように文字列長をチェックして1桁なら頭に"0"を付与するという処理を愚直に繰り返しています。

#if($second.toString().length() == 1)
  #set($second = "0" + $second.toString())
#else
  #set($second = $second.toString())
#end

他に良いやり方をご存知の方がいれば教えてください

まとめ

Lambdaを使いましょう

参考

Share this article

facebook logohatena logotwitter logo

© Classmethod, Inc. All rights reserved.