Closure Templates 2.4の新機能 - パラメータの型チェック

2014年4月にリリースされたClosure Templates 2.4では、いくつかの面白い新機能が追加されています。
今回はテンプレートのパラメータの型チェック機能について紹介します。

Closure Templatesの基本的な使い方についてはこちらをご覧ください。

- [Closure Templatesことはじめ - Java編](http://qiita.com/zoetro/items/4011d4bd2f1acf6e0684)

# `{@param}`

今までのClosure Templatesでは、テンプレート内で使用するパラメータをコメントに記述する必要がありました。

~~~hello.soy
/**
 * @param name
 */
{template .render}
  Hello, {$name}!
{/template}
~~~
 
Closure Templates 2.4では、コメントではなくテンプレートの記法としてパラメータを記述することができるようになりました。（このときコメントの`@param`は不要です）
そしてパラメータ名の後ろに型を指定することができます。

~~~hello.soy
/**
 */
{template .render}
  {@param name: string}
  Hello, {$name}!
{/template}
~~~

指定できる型としては、stringやintなどのプリミティブ型をはじめとして list, map, union, recordなどがあります。
詳しくは公式ドキュメントをご覧ください。

- https://developers.google.com/closure/templates/docs/commands?hl=ja#param

型を指定すると、テンプレートファイルのコンパイル時と、レンダリングの実行時に型のチェックがおこなわれるようになります。
それぞれどのようなチェックがおこなわれるのかみてみましょう。

# コンパイル時の型チェック

これまでのClosure Templatesでは、テンプレート内で使用しているパラメータをコメントに書いていなかったり、逆にコメントに書いてあるパラメータがテンプレート内で使われていないと、テンプレートファイルのコンパイル時にエラーが発生しました。
Closure Templates 2.4では、さらに型が一致しているかどうかのチェックもコンパイル時におこなってくれるようになりました。

たとえば、以下のように`.parent`で`string`型の`name`という変数を、`.child`の`int`型のパラメータに渡すようなテンプレートを書いてみます。

~~~hello.soy
{namespace example.templates.hello}

/**
 * parent template
 */
{template .parent}
  {@param name: string}
  {call .child}
    {param name: $name /}
  {/call}
{/template}

/**
 * child template
 */
{template .child}
  {@param name: int}
  Hello, {$name}!
{/template}
~~~

このテンプレートファイルを次のようにコンパイルします。

~~~java
SoyTofu tofu = SoyFileSet.builder()
    .add(new File("hello.soy"))
    .build()
    .compileToTofu();
~~~

すると型が一致していないというエラーになります。いい感じですね。

~~~
com.google.template.soy.base.SoySyntaxException: In file hello.soy:8, template example.templates.hello.parent: Argument type mismatch: cannot call template parameter 'name' with type 'int' with value of type 'string'
~~~

また、次のテンプレートファイルのように`int`型のパラメータのプロパティにアクセスしようとしたり、`record`型のパラメータの存在しないプロパティにアクセスしようとした場合もコンパイルエラーになります。

~~~property.soy
{namespace example.templates.property}

/**
 */
{template .render}
  {@param x: int}
  {@param y: [a: int, b:int]}

  // コンパイルエラー
  {{$x.a}}

  // OK
  {{$y.a + $y.b}}

  // コンパイルエラー
  {{$y.c}}
{/template}
~~~

でも、次のように`string`型に対して数値演算しようとしてもコンパイルエラーにはならず、実行時エラーになります。ここはもう少し頑張って欲しいところですね。

~~~operate.soy
{namespace example.templates.operate}

/**
 */
{template .render}
  {@param x: string}
  {@param y: int}

  // コンパイルエラーにならず、実行時エラーになる
  x * y = {{$x * $y}}

{/template}
~~~

# 実行時の型チェック

続いて実行時の型チェックの挙動についてみてみましょう。

まずは、次のように`string`型のパラメータを持つテンプレートを定義します。

~~~hello.soy
{namespace example.templates.hello}

/**
 * hello template
 */
{template .render}
  {@param name: string}
  Hello, {$name}!
{/template}
~~~

このテンプレートの`name`パラメータに文字列を与えてレンダリングしてみます。

~~~java
SoyMapData data = new SoyMapData("name", "World");
String output = tofu.newRenderer("example.templates.hello.render")
    .setData(data)
    .render();
~~~

これは問題なくレンダリングされます。

次に`name`パラメータに数値を与えてレンダリングしてみます。

~~~java
SoyMapData data = new SoyMapData("name", 1234);
String output = tofu.newRenderer("example.templates.hello.render")
    .setData(data)
    .render();
~~~

このコードを実行すると次のような例外が発生してレンダリングに失敗します。
ちなみにJavaScriptで実行した場合も、assertにより例外が発生します。

~~~
com.google.template.soy.tofu.SoyTofuException: Parameter type mismatch: attempt to bind value '1234' to parameter 'name' which has declared type 'string'.
	at example.templates.hello.render(hello.soy:5)
~~~

# まとめ

テンプレートのパラメータの型チェック機能を利用すれば、テンプレートファイルの不具合に早く気づくことができるようになるでしょう。
積極的に利用していきたい機能です。

なお、今回利用したサンプルコードはGitHubにもアップしてあります。

 - https://github.com/zoetrope/closure-sandbox/tree/master/typed_sample