EExでconfigファイルのテンプレート化

10月からPerlの国から越してきました@toku_bassです。
この記事はAdventCalender 2017の12日目の記事になります。

# テンプレート化の動機
`my_project/config/`配下のファイルが環境(dev,stg,prod)毎に手書きでメンテしている状態を解決したかったのでtemplateファイルから生成することにしました。
いくらCIでテストを回しても、prod.exsはテストできている訳ではないので手書きは怖いです。

# EEx
まずEExの解説から。

https://elixirschool.com/jp/lessons/specifics/eex/
> RubyにERBが、そしてJavaにJSPがあるようにElixirにもEEx即ち埋め込みElixirがあります。EExを使って文字列の中にElixirを埋め込んで評価することができます。

```ex
iex> EEx.eval_string("<%= foo %>", foo: 1)
"1"
```

eval_stringの中を追って行くと、
まずtemplateをtokenに分解して、それぞれのtokenに対応したhandlerがtokenをElixirのコードに置き換えていきます。
handlerはEEx.Engineに定義されており、自作Engineに差し替えて挙動を変えることができます。

```ex
# イメージ
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をします
挙動としては以下のようになっています。

```ex
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テンプレートとして使えるのか試してみます。

## 単純なデータ
```ex
iex> EEx.eval_string("<%= foo %>", foo: 1)
"1"
```
はい


```ex
iex> EEx.eval_string("<%= foo %>", foo: "a")
"a"
iex> EEx.eval_string("<%= foo %>", foo: 'a')
"a"
iex> EEx.eval_string("<%= foo %>", foo: :a)
"a"
```
あれ、、、

```ex
EEx.eval_string("<%= foo %>", foo: ["a","b","c"])
"abc"
```
あらら

config/*.exsファイルを生成したいので異なるデータにされるのは困ります。
`EEx.eval_string("<%= foo %>", foo: ":a")`
とすれば`:a`が表示されますが、全部文字列で持つのはデータファイルの管理上うれしくありません。
Hexで他のテンプレートエンジンも探してみましたが、stringに変換する挙動は同じでした。


## 条件分岐

条件分岐の使い勝手はどうでしょうか

```ex
iex> EEx.eval_string("<%= if false do %><%= hoge %><% end %>", [])
** (CompileError) nofile:1: undefined function hoge/0
```
if falseなのにhogeを要求されてしまいました。
これは(説明は割愛しますが)assignsを使えば大丈夫です。

```ex
EEx.eval_string("<%= if false do %><%= @hoge %> <% end %>", assigns: [])
```

## templateの改行削除
比較的新しいバージョンのElixirではtrimオプションがあります

```ex
iex> EEx.eval_string("start
...> <%= if false do %>
...>  <%= foo %>
...> <% end %>
...> end", [foo: 1], trim: true)

"start\nend"
```
想定通り

しかしifを1行にすると、、、

```ex
iex> EEx.eval_string("start
...> <%= if false do %><%= foo %><% end %>
...> end", [foo: 1], trim: true)

"start\n\nend"
```
これはバグっぽい、、

# Engineを差し替えて対処する

示した例のように、[to_stringで変換される](https://github.com/elixir-lang/elixir/blob/v1.5.2/lib/eex/lib/eex/engine.ex#L165)と上手くいかないのでinspectで出力して期待通り表示されるようにします。


```ex
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を作ります。

```ex
defmodule Mix.MyProject.Config.DevConstant do

  @config [
    logger: %{
      level: :debug,
      truncate: 1024
    },
  ]

  def get_all do
    @config
  end

end
```

```ex
# config/template.eex
use Mix.Config

config :logger,
 level: <%= @logger.level %>,
 truncate: <%= @logger.truncate %>
```

```ex
# 実際は 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 について」です