Elixirで設定ファイルをロードする(JSON, YAML, TOML)

# はじめに

この記事では、Elixirを用いた各種設定ファイルのロード方法やTipsについて説明します。
なお、以降の説明は、以下のようなディレクトリ構成で各ファイルが配置されているという前提のもとで進めます。

```
config
  |-setting.json
  |-setting.toml
  |-setting.yml
lib
  |-loader.ex
```


# JSON

まずはJSON形式のファイルロードについて考えてみます。
JSONの設定ファイルには、次のような値が格納されているとしましょう。

```json:setting.json
{
  "job": {
    "name": "new_job",
    "step": [
      {"level": 1, "desc": "Start"},
      {"level": 2, "desc": "Stop"}
    ],
    "repeat": false
  }
}
```

このようなJSON形式の文字列をパースす場合は [Poison](https://github.com/devinus/poison) を用いることができます。

次のように deps を追加し、Poisonによって読み込んだ文字列をオブジェクトへとパースします。

```elixir:mix.exs
  defp deps do
    [{:poison, "~> 3.1"}]
  end
```

```elixir:loader.ex
  def load_json() do
    "./config/setting.json"
    |> File.read!()
    |> Poison.decode!()
  end
```

上記のコードを実行すると、次の結果が得られます。

```elixir:iex
iex> Loader.load_json()
%{
  "job" => %{
    "name" => "new_job",
    "repeat" => false,
    "step" => [
      %{"desc" => "Start", "level" => 1},
      %{"desc" => "Stop", "level" => 2}
    ]
  }
}
```

# YAML

次にYAMLを使うケースについて考えてみましょう。
次のようなYAMLの設定ファイルがあるとします。

```yml:setting.yml
job:
  name: new_job
  step:
  - level: 1
    desc: Start
  - level: 2
    desc: Stop
  repeat: false
```

このような YAML で定義されたファイルの内容を取得する場合、 [YamlElixir](https://github.com/KamilLelonek/yaml-elixir) を使うことができます。
以下のように、deps の追加と関数の定義を行いましょう。

```elixir:mix.exs
  defp deps do
    [{:yaml_elixir, "~> 2.1"}]
  end
```

```elixir:loader.ex
  def load_yaml() do
    "./config/setting.yml"
    |> File.read!()
    |> YamlElixir.read_from_string()
  end
```

このコードを実行すると、次のように YAML がパースされたオブジェクトが得られます。

```elixir:iex
iex> Loader.load_yaml()
%{
  "job" => %{
    "name" => "new_job",
    "repeat" => false,
    "step" => [
      %{"desc" => "Start", "level" => 1},
      %{"desc" => "Stop", "level" => 2}
    ]
  }
}
```

また、YamlElixirは `read_from_file/1` や `read_from_file!/1` といった関数も提供しているので、次のようにファイルの読み込みとオブジェクトへのパースをまとめて行うこともできます。

```elixir:loader.ex
  def load_yaml_from_file() do
    "./config/setting.yml"
    |> YamlElixir.read_from_file!()
  end
```



# TOML

[TOML](https://github.com/toml-lang/toml) は設定を定義するためのファイルフォーマットであり、ハッシュテーブルへと明確にマップすることが可能であるという特徴を持ちます。これまで登場したような設定は、以下のように定義されます。

```toml:setting.toml
[job]
name = "new_job"
repeat = false

  [[job.step]]
  level = 1
  desc = "Start"

  [[job.step]]
  level = 2
  desc = "Stop"
```

(ここでは TOML 自体の説明は割愛しますが、 [設定ファイル記述言語 TOML](https://qiita.com/b4b4r07/items/77c327742fc2256d6cbe) のQiita記事が詳しく説明していますので参考にしてみてください)

上記のようなTOMLの設定をロードするには、 [Toml](https://github.com/bitwalker/toml-elixir) というElixirライブラリを利用することができます。

次のように、 deps の追加、関数の定義をしましょう。

```elixir:mix.exs
  defp deps do
    [{:toml, "~> 0.3"}]
  end
```

```elixir:loader.ex
  def load_toml() do
    "./config/setting.toml"
    |> File.read!()
    |> Toml.decode!()
  end
```

このコードにより、次のようにTOMLの設定値を取り出すことができます。

```elixir:iex
iex(10)> Loader.load_toml()
%{
  "job" => %{
    "name" => "new_job",
    "repeat" => false,
    "step" => [
      %{"desc" => "Start", "level" => 1},
      %{"desc" => "Stop", "level" => 2}
    ]
  }
}
```

また、 Toml はファイルから直接オブジェクトを取り出すための関数 `decode_file!` も提供しているので、次のように書くこともできます。

```elixir:loader.ex
  def load_toml_from_file() do
    "./config/setting.toml"
    |> Toml.decode_file!()
  end
```

# ビルド時の設定ロード

これまで紹介した方法は、コードが実行される際にファイルの内容を読み出してオブジェクトへとパースするという方法を取ってきました。しかし、この方法は実行毎にファイルIOが発生するため、頻繁に実行される処理などでは好ましいものではありません。また、設定ファイルの内容を反映させた上でDistilleryなどで配布したいうというケースもあるでしょう。

このようなケースでは、ビルドの時点でファイルから設定を読み込んでおき、モジュールの属性(attribute)として定義しておくという方法を取ることができます。次のコードを見てみましょう。

```elixir:loader.ex
defmodule Loader do
  @json_config "./config/setting.json" |> File.read!() |> Poison.decode!()
  def json_config(), do: @json_config
end
```

このコードをビルドすることで、 `setting.json` から読み出した内容を `@json_config` へと反映させます。そのため、 `Loader.json_config` を実行すると、ファイルIOを発生させることなく特定の設定ファイルの内容を取得することができるようになります。

# dev環境のみ動的読み込み

前述した方法では、ビルド時に設定内容をロードしておくことでファイルアクセス無しに設定内容を取得することができました。しかし、これは逆に言うと、ビルドをし直さない限り設定ファイルの内容が反映されないということを意味します。これは、開発時のように設定を頻繁に変更してトライアンドエラーを繰り返すようなケースでは都合が悪く、ビルド無しで毎回設定ファイルを読み込んでくれた方が効率が良いでしょう。

そこで、以下のようにすると、dev環境でのみ設定ファイルを毎回ロードさせることができます。

```elixir:loader.ex
defmodule Loader do
  if Mix.env == :dev do
    def json_config() do
      "./config/setting.json" |> File.read!() |> Poison.decode!()
    end
  else
    @json_config "./config/setting.json" |> File.read!() |> Poison.decode!()
    def json_config(), do: @json_config
  end
end
```

見るとわかるように、 `Mix.env` によって得られる値が `:dev` であるかどうかによってビルドされるコードが決定されます。そのためこのケースでは、dev でのみ設定ファイルを都度読み込む処理となります。
(他にも [Dependency Injection](https://www.openmymind.net/Dependency-Injection-In-Elixir/) を利用するという方法を取ることもできます)