LoginSignup
7
5

More than 5 years have passed since last update.

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

Last updated at Posted at 2017-12-12

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 について」です

7
5
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
5