10月からPerlの国から越してきました@toku_bassです。
この記事はAdventCalender 2017の12日目の記事になります。
テンプレート化の動機
my_project/config/
配下のファイルが環境(dev,stg,prod)毎に手書きでメンテしている状態を解決したかったのでtemplateファイルから生成することにしました。
いくらCIでテストを回しても、prod.exsはテストできている訳ではないので手書きは怖いです。
EEx
まずEExの解説から。
RubyにERBが、そしてJavaにJSPがあるようにElixirにもEEx即ち埋め込みElixirがあります。EExを使って文字列の中にElixirを埋め込んで評価することができます。
iex> EEx.eval_string("<%= foo %>", foo: 1)
"1"
eval_stringの中を追って行くと、
まずtemplateをtokenに分解して、それぞれのtokenに対応したhandlerがtokenをElixirのコードに置き換えていきます。
handlerはEEx.Engineに定義されており、自作Engineに差し替えて挙動を変えることができます。
# イメージ
def eval_string do
compiled = compile(string)
do_eval(compiled, bindings) #値のbinding
end
def compile do
tokens = tokenize()
token_handler(tokens)
end
ここまでがtemplateのcompileになり、この後evalでbindingをします
挙動としては以下のようになっています。
iex > compiled = quote do: unquote({:foo, [line: 1]), nil}
{:foo, [line: 1], nil}
iex > Code.eval_quoted(compiled, [foo: 100])
{100, [foo: 100]}
tokenizeの詳細も書きたかったのですが、長くなるので割愛します。
configテンプレートとして使う
さて、ではこのEExがConfigテンプレートとして使えるのか試してみます。
単純なデータ
iex> EEx.eval_string("<%= foo %>", foo: 1)
"1"
はい
iex> EEx.eval_string("<%= foo %>", foo: "a")
"a"
iex> EEx.eval_string("<%= foo %>", foo: 'a')
"a"
iex> EEx.eval_string("<%= foo %>", foo: :a)
"a"
あれ、、、
EEx.eval_string("<%= foo %>", foo: ["a","b","c"])
"abc"
あらら
config/*.exsファイルを生成したいので異なるデータにされるのは困ります。
EEx.eval_string("<%= foo %>", foo: ":a")
とすれば:a
が表示されますが、全部文字列で持つのはデータファイルの管理上うれしくありません。
Hexで他のテンプレートエンジンも探してみましたが、stringに変換する挙動は同じでした。
条件分岐
条件分岐の使い勝手はどうでしょうか
iex> EEx.eval_string("<%= if false do %><%= hoge %><% end %>", [])
** (CompileError) nofile:1: undefined function hoge/0
if falseなのにhogeを要求されてしまいました。
これは(説明は割愛しますが)assignsを使えば大丈夫です。
EEx.eval_string("<%= if false do %><%= @hoge %> <% end %>", assigns: [])
templateの改行削除
比較的新しいバージョンのElixirではtrimオプションがあります
iex> EEx.eval_string("start
...> <%= if false do %>
...> <%= foo %>
...> <% end %>
...> end", [foo: 1], trim: true)
"start\nend"
想定通り
しかしifを1行にすると、、、
iex> EEx.eval_string("start
...> <%= if false do %><%= foo %><% end %>
...> end", [foo: 1], trim: true)
"start\n\nend"
これはバグっぽい、、
Engineを差し替えて対処する
示した例のように、to_stringで変換されると上手くいかないのでinspectで出力して期待通り表示されるようにします。
defmodule MyEngine do
use EEx.Engine
def handle_expr(buf, "=", expr) do
expr = Macro.prewalk(expr, &EEx.Engine.handle_assign/1) # assigns用
quote do
unquote(buf) <> inspect(unquote(expr))
end
end
end
IO.puts EEx.eval_string("<%= foo %>", [ foo: :a ], engine: MyEngine)
# :a
IO.puts EEx.eval_string("<%= foo %>", [ foo: "a" ], engine: MyEngine)
# "a"
IO.puts EEx.eval_string("<%= foo %>", [ foo: 'a' ], engine: MyEngine)
# 'a'
IO.puts EEx.eval_string("<%= foo %>", [ foo: ["a","b", "c"] ], engine: MyEngine)
# ["a","b","c"]
成果物
準備が整ったので、configを作ります。
defmodule Mix.MyProject.Config.DevConstant do
@config [
logger: %{
level: :debug,
truncate: 1024
},
]
def get_all do
@config
end
end
# config/template.eex
use Mix.Config
config :logger,
level: <%= @logger.level %>,
truncate: <%= @logger.truncate %>
# 実際は mix task にしています
template = File.read!("config/template.eex")
params = Mix.MyProject.Config.DevConstant.get_all()
config = EEx.eval_string(template, [assigns: params], engine: MyEngine)
File.write!("config/dev.exs",config)
以上で12日のAdventCalendarの記事を終わります。
ありがとうございました。
明日は
@rinosamakanataさんの「erlyberly について」です