Phoenix軽量APIを使ったREST APIのコード（データはMnesia DBに保持）

MVC／ルーティング一切不要、JSONテンプレートをフォルダ配置するだけで、APIが開発できる「Phoenix軽量API」の構築は、下記コラムシリーズをご覧ください

[<b>①PHP的ハックを応用してNode.js Express／Go的な軽量APIをPhoenixで実現してみた</b>](https://qiita.com/piacerex/items/144089aa6f2052f4d1cb)
|> [<b>②Express／Go的なPhoenix軽量APIの上に参照系REST APIを実装する</b>](https://qiita.com/piacerex/items/e11ef7b5cdcc81ac69c9)
|> [<b>③軽量APIにて実装した一覧REST APIにDBを接続する</b>](https://qiita.com/piacerex/items/437e7b6c26c95dae0efb)
|> [<b>④DB取得＋軽量APIで1件参照REST APIを実装する(TDD付)</b>](https://qiita.com/piacerex/items/7c969e42e7cf4fa5ed50)
|> [<b>⑤DB追加／削除＋軽量APIで更新系REST API（暫定版）を実装する(TDD付)</b>](https://qiita.com/piacerex/items/94d9275e32ecd0e83da1)
|> [<b>⑥軽量APIの追加・削除REST APIを完成させる</b>](https://qiita.com/piacerex/items/2c95ff8f01d4a9d2d241)
|> [<b>⑦更新REST APIの実装で軽量REST API実装完成(TDD付)</b>](https://qiita.com/piacerex/items/78d3036e556e66981c79)
|> [<b>⑧エラー処理もJSONテンプレートで簡単に＆コード最適化</b>](https://qiita.com/piacerex/items/9e507ae2f75c1e2edefd)

# 軽量API JSONテンプレート

Phoenix軽量APIを使うと、これらJSONテンプレートをPhoenixのtemplatesフォルダ配下に、任意のフォルダ階層で置くだけで、エンドポイント付きのJSON APIが構築できます

create.json.eex／update.json.eex／delete.json.eexは、``{ :ok, 【結果】 }``か``{ :error, 【結果】 }``の形式での返却を前提にしています

```elixir:lib/basic_web/templates/api/v1/users/data.json.eex
%{
  name:     data[ "name" ], 
  age:      data[ "age" ], 
  team:     data[ "team" ], 
  position: data[ "position" ]
}
```

```elixir:lib/basic_web/templates/api/v1/users/index.json.eex
json = File.read!( "lib/basic_web/templates/api/v1/users/data.json.eex" ) 
datas = Db.query( "select * from members" ) |> Db.columns_rows

for data <- datas do
  json
  |> Code.eval_string( [ params: params, data: data ] )
  |> elem( 0 )
end
```

```elixir:lib/basic_web/templates/api/v1/users/show.json.eex
datas = Db.query( "select * from members where id = #{ params[ "id" ] }" ) |> Db.columns_rows
if datas == [] || Enum.count( datas ) > 1 do
	{ :not_found, "Not Found" |> Rest.error_body }
else
	File.read!( "lib/basic_web/templates/api/v1/users/data.json.eex" ) 
	|> Code.eval_string( params: params, data: data )
	|> elem( 0 )
end
```

```json:lib/basic_web/templates/api/v1/users/create.json.eex
data = params[ "data" ]
Db.query( "insert into members values( '#{ data[ "name" ] }', #{ data[ "age" ] }, '#{ data[ "team" ] }', '#{ data[ "position" ] }' )" )
```

```json:lib/basic_web/templates/api/v1/users/update.json.eex
data = params[ "data" ]
Db.query( "update members set name = '#{ data[ "name" ] }', age = #{ data[ "age" ] }, team = '#{ data[ "team" ] }', position = '#{ data[ "position" ] }' where id = #{ params[ "id" ] }" )
```

```json:lib/basic_web/templates/api/v1/users/delete.json.eex
Db.query( "delete from members where id = #{ params[ "id" ] }" )
```

# ルータ

```elixir:lib/basic_web/router.ex
defmodule BasicWeb.Router do
…
  scope "/api/", BasicWeb do
    pipe_through :browser

    # v-- add start
    get    "/*path_", ApiController, :index
    post   "/*path_", ApiController, :create
    put    "/*path_", ApiController, :update
    delete "/*path_", ApiController, :delete
    # ^-- add end
  end
…
```

# APIコントローラ

※軽量APIを使うだけの方は、以降のコードは理解しなくてもOKです

```elixir:lib/basic_web/controllers/api_controller.ex
defmodule BasicWeb.ApiController do
  use BasicWeb, :controller

  def index( conn, params ) do
    { no_id_path, id } = Rest.separate_id( params[ "path_" ] )
    { new_params, template } = 
      if id == nil do
        { 
          params, 
          "index.json", 
        }
      else
        { 
          params |> Map.put( "id", id ), 
          "show.json", 
        }
      end
    try do
      result = execute( "#{ no_id_path }#{ template }", params: new_params )
      if is_tuple( result ) do
        response( conn, elem( result, 0 ), elem( result, 1 ) |> Jason.encode! )
      else
        response( conn, :ok, result |> Jason.encode! )
      end
    rescue 
      err -> 
        response( conn, :internal_server_error, err |> inspect |> Rest.error_body )
    end
  end

  def create( conn, params ) do
    { no_id_path, _ } = Rest.separate_id( params[ "path_" ] )

    # TODO：data.json.eexと列突合チェック
    try do
      result = execute( "#{ no_id_path }create.json", params: params )

      if elem( result, 0 ) == :ok do
        new_params = params |> Map.put( "id", elem( result, 1 ) )

        response = execute( "#{ no_id_path }show.json", params: new_params )
        response( conn, :created, response |> Jason.encode! )
      else
        response( conn, :internal_server_error, elem( result, 1 ) |> inspect |> Rest.error_body )
      end
    rescue 
      err -> 
        response( conn, :internal_server_error, err |> inspect |> Rest.error_body )
    end
  end

  def update( conn, params ) do
    { no_id_path, id } = Rest.separate_id( params[ "path_" ] )
    if id == nil do
      response( conn, :not_found, %{ error: "Not Found" } |> Jason.encode! )
    else
      new_params = params |> Map.put( "id", id )

      # TODO：data.json.eexと列突合チェック
      try do
        result = execute( "#{ no_id_path }update.json", params: new_params )

        if elem( result, 0 ) == :ok do
          response = execute( "#{ no_id_path }show.json", params: new_params )
          response( conn, :ok, response |> Jason.encode! )
        else
          response( conn, :internal_server_error, elem( result, 1 ) |> inspect |> Rest.error_body )
        end
      rescue 
        err -> 
          response( conn, :internal_server_error, err |> inspect |> Rest.error_body )
      end
    end
  end

  def delete( conn, params ) do
    { no_id_path, id } = Rest.separate_id( params[ "path_" ] )
    if id == nil do
      response( conn, :not_found, "Not Found" |> Rest.error_body )
    else
      new_params = params |> Map.put( "id", id )

      # TODO：data.json.eexと列突合チェック
      try do
        _result = execute( "#{ no_id_path }delete.json", params: new_params )
        response( conn, :no_content, "" )
      rescue 
        err -> 
          response( conn, :internal_server_error, err |> inspect |> Rest.error_body )
      end
    end
  end

  def execute( path, params: params ) do
    File.read!( "lib/basic_web/templates/api/#{ path }.eex" )
    |> Code.eval_string( params: params, data: params[ "data" ] ) 
    |> elem( 0 )
  end

  def response( conn, status, body ) do
    conn
    |> put_resp_header( "content-type", "application/json; charset=utf-8" )
    |> send_resp( status, body )
  end
end
```

# ビュー

```elixir:lib/basic_web/views/api_view.ex
defmodule BasicWeb.ApiView do
  use BasicWeb, :view
end
```

# Restモジュール

※軽量APIを使うだけの方は、以降のコードは理解しなくてもOKです

```elixir:lib/util/rest.ex
defmodule Rest do
	@doc """
	Path list devide last id

	## Examples
	iex> Rest.separate_id( [ "abc", "def", "123" ] )
	{ "abc/def/", 123 }
	iex> Rest.separate_id( [ "456" ] )
	{ "", 456 }
	iex> Rest.separate_id( [ "abc", "def" ] )
	{ "abc/def/", nil }
	iex> Rest.separate_id( [] )
	{ "", nil }
	iex> Rest.separate_id( [ "" ] )
	{ "", nil }
	iex> Rest.separate_id( nil )
	{ "", nil }
	"""
	def separate_id( nil ), do: { "", nil }
	def separate_id( path_list ) when is_list( path_list ) do
		id = path_list |> List.last |> Type.to_number
		no_id_path_list = if id == nil, do: path_list, else: path_list |> Enum.drop( -1 )
		{ no_id_path_list |> concat_path, id }
	end

	@doc """
	Path list concat to path string

	## Examples
	iex> Rest.concat_path( [ "abc", "def" ] )
	"abc/def/"
	iex> Rest.concat_path( [] )
	""
	iex> Rest.concat_path( [ "" ] )
	""
	iex> Rest.concat_path( [ "", "" ] )
	""
	"""
	def concat_path( [] ), do: ""
	def concat_path( path_list ) when is_list( path_list ) do
		if List.first( path_list ) == "" do
			""
		else
			Enum.join( path_list, "/" ) <> "/" 
		end
	end

	@doc """
	Error body
	"""
	def error_body( body ), do: %{ error: body } |> Jason.encode!
end
```

# Mnesia DBアクセッサ

※軽量APIを使うだけの方は、以降のコードは理解しなくてもOKです

```elixir:lib/util/db.ex
defmodule Db do
  def query(sql) when sql != "" do
    cond do
      String.match?(sql, ~r/select.*/) -> select(sql)
      String.match?(sql, ~r/insert.*/) -> insert(sql)
      String.match?(sql, ~r/update.*/) -> update(sql)
      String.match?(sql, ~r/delete.*/) -> delete(sql)
    end
  end

  def select(sql) do
    if String.match?(sql, ~r/.*where.*/) do
      parsed_map = Regex.named_captures(~r/select( *)(?<columns>.*)( *)from( *)(?<tables>.*)( *)where( *)(?<wheres>.*)/, sql)
      DbMnesia.select(
        parsed_map["tables"]  |> string_to_list |> List.first, 
        parsed_map["columns"] |> string_to_list, 
        parsed_map["wheres"]  |> string_to_list)
    else
      parsed_map = Regex.named_captures(~r/select( *)(?<columns>.*)( *)from( *)(?<tables>.*)/, sql)
      DbMnesia.select(
        parsed_map["tables"]  |> string_to_list |> List.first, 
        parsed_map["columns"] |> string_to_list, 
        [])
    end
  end

  def insert(sql) do
    if false do
      parsed_map = Regex.named_captures(~r/insert( *)into( *)(?<tables>.*)\(( *)(?<columns>.*)( *)\)( *)values\(( *)(?<values>.*)( *)\)/, sql)
      DbMnesia.insert(
        parsed_map["tables"]  |> string_to_list |> List.first, 
        parsed_map["columns"] |> string_to_list, 
        parsed_map["values"]  |> string_to_list)
    else
      parsed_map = Regex.named_captures(~r/insert( *)into( *)(?<tables>.*)( *)values\(( *)(?<values>.*)( *)\)/, sql)
      DbMnesia.insert(
        parsed_map["tables"]  |> string_to_list |> List.first, 
        [], 
        parsed_map["values"]  |> string_to_list)
    end
  end

  def update(sql) do
    parsed_map = Regex.named_captures(~r/update( *)(?<tables>.*)( *)set( *)(?<sets>.*)( *)where( *)(?<wheres>.*)/, sql)
    DbMnesia.update(
      parsed_map["tables"]  |> string_to_list |> List.first, 
      parsed_map["sets"  ]  |> string_to_list, 
      parsed_map["wheres"]  |> string_to_list)
  end

  def delete(sql) do
    if String.match?(sql, ~r/.*where.*/) do
      parsed_map = Regex.named_captures(~r/delete( *)from( *)(?<tables>.*)( *)where( *)(?<wheres>.*)/, sql)
      DbMnesia.delete(
        parsed_map["tables"]  |> string_to_list |> List.first, 
        parsed_map["wheres"]  |> string_to_list)
    else
      parsed_map = Regex.named_captures(~r/delete( *)from( *)(?<tables>.*)/, sql)
      DbMnesia.delete(
        parsed_map["tables"]  |> string_to_list |> List.first, 
        [])
    end
  end

  @doc """
  String to list(item trimmed)

  ## Examples
      iex> Db.string_to_list("id = 123, name = 'hoge'")
      ["id = 123", "name = 'hoge'"]
      iex> Db.string_to_list("team = 'foo'")
      ["team = 'foo'"]
      iex> Db.string_to_list("")
      [""]
  """
  def string_to_list(string), do: string |> String.split(",") |> Enum.map(& String.trim(&1))

  def columns_rows(result) do
    result
    |> rows
    |> Enum.map(fn row -> Enum.into(List.zip([columns(result), row]), %{}) end)
  end
  def rows(%{ rows: rows } = _result), do: rows
  def columns(%{ columns: columns } = _result), do: columns
end
```

```elixir:lib/util/db_mnesia.ex
defmodule DbMnesia do
  def insert(table_name, _columns, values) do
    # TODO: 列選択はそのうち
    :mnesia.start
    table_atom = String.to_atom(table_name)
    :mnesia.wait_for_tables([table_atom], 1000)
    result = :mnesia.transaction(fn -> 
      next_id = max_id(table_atom) + 1
      insert_spec = values |> values_value |> Tuple.insert_at(0, next_id) |> Tuple.insert_at(0, table_atom)
      writed = :mnesia.write(insert_spec)
      {writed, next_id}
    end)
    case result do
      {:atomic,  {:ok, id}} -> {:ok,    id }
      {:aborted, {err, _}}  -> {:error, err}
      {_,        {err, _}}  -> {:error, err}
                 {err, _}   -> {:error, err}
    end
  end

  def max_id(table_atom) do
    {:atomic, keys} = :mnesia.transaction(fn -> :mnesia.all_keys(table_atom) end)
    case keys do
      [] -> -1
      _  -> keys |> Enum.max
    end
  end

  def update(table_name, sets, wheres) do
    :mnesia.start
    table_atom = String.to_atom(table_name)
    :mnesia.wait_for_tables([table_atom], 1000)
    where_value = wheres |> wheres_value |> elem(0)
    update_spec = sets |> sets_value |> Tuple.insert_at(0, where_value) |> Tuple.insert_at(0, table_atom)
    result = :mnesia.transaction(fn -> :mnesia.write(update_spec) end)
    case result do
      {:atomic,  :ok} -> {:ok}
      {:aborted, err} -> {:error, err}
      {_,        err} -> {:error, err}
      err             -> {:error, err}
    end
  end

  def delete(table_name, wheres) do
    :mnesia.start
    table_atom = String.to_atom(table_name)
    :mnesia.wait_for_tables([table_atom], 1000)
    where_spec = wheres |> wheres_value |> Tuple.insert_at(0, table_atom)
    result = :mnesia.transaction(fn -> :mnesia.delete(where_spec) end)
    case result do
      {:atomic,  :ok} -> {:ok}
      {:aborted, err} -> {:error, err}
      {_,        err} -> {:error, err}
      err             -> {:error, err}
    end
  end

  def select(table_name, _columns, wheres) do
    # TODO: 列選択はそのうち
    :mnesia.start
    table_atom = String.to_atom(table_name)
    :mnesia.wait_for_tables([table_atom], 1000)
    columns = :mnesia.table_info(table_atom, :attributes) |> Enum.map(& Atom.to_string(&1))
    specs = [table_atom] ++ Enum.map(1..Enum.count(columns), & :"$#{&1}") |> List.to_tuple
    wheres_spec = wheres |> wheres_param |> wheres_spec(columns, specs)
    rows = :mnesia.transaction(fn -> 
      :mnesia.select(table_atom, [{specs, wheres_spec, [:"$$"]}]) end) |> elem(1)
    %{
      columns: columns, 
      command: :select, 
      connection_id: 0, 
      num_rows: Enum.count(rows), 
      rows: rows
   }
  end

  @doc """
  SQL Where strings(list) to Mnesia conditons(tuple list), but different only for columns name

  ## Examples
    iex> DbMnesia.wheres_param(["id = 123", "name = 'hoge'", "team = 'foo'"])
    [
      {:==, "id",   123   }, 
      {:==, "name", "hoge"}, 
      {:==, "team", "foo" } 
   ]
    iex> DbMnesia.wheres_param(["name = 'hogehoge'", "position = 'bar'"])
    [
      {:==, "name",     "hogehoge"}, 
      {:==, "position", "bar" } 
   ]
  """
  # TODO: 「=」以外の演算にも対応したい
  def wheres_param(wheres) do
    wheres
    |> Enum.map(fn where ->
      kv = String.split(where, "=") |> Enum.map(& String.trim(&1))
      v = List.last(kv)
      {:==, List.first(kv), raw_value(v)}
    end)
  end

  @doc """
  SQL Values strings(list) to value list

  ## Examples
    iex> DbMnesia.values_value(["123", "'hoge'"])
    {123, "hoge"} 
    iex> DbMnesia.values_value(["987", "'foo'"])
    {987, "foo"} 
  """
  # TODO: Keys指定からTableKeysの並び順に直したValuesを返却して、呼出側で列バインドしたい
  def values_value(values) do
    values 
    |> Enum.reduce({}, fn v, acc ->
      Tuple.append(acc, raw_value(v))
    end)
  end

  @doc """
  SQL Sets strings(list) to value list

  ## Examples
    iex> DbMnesia.sets_value(["id = 123", "name = 'hoge'", "team = 'foo'"])
    {123, "hoge", "foo"} 
    iex> DbMnesia.sets_value(["name = 'hogehoge'", "position = 'bar'"])
    {"hogehoge", "bar"} 
  """
  # TODO: KeyValueを返却して、呼出側で列バインドしたい
  def sets_value(sets) do
    sets
    |> Enum.reduce({}, fn set, acc ->
      kv = String.split(set, "=") |> Enum.map(& String.trim(&1))
      v = List.last(kv)
      Tuple.append(acc, raw_value(v))
    end)
  end

  @doc """
  SQL Where strings(list) to value list

  ## Examples
    iex> DbMnesia.wheres_value(["id = 123"])
    {123} 
    iex> DbMnesia.wheres_value(["id = 987"])
    {987} 
  """
  def wheres_value(wheres) do
    wheres
    |> Enum.reduce({}, fn where, acc ->
      kv = String.split(where, "=") |> Enum.map(& String.trim(&1))
      v = List.last(kv)
      Tuple.append(acc, raw_value(v))
    end)
  end

  @doc """
  Mnesia conditons columns name convert to params

  ## Examples
    iex> DbMnesia.wheres_spec([{:==, "id", 123}, {:==, "team", "foo"}], ["id", "name", "age", "team", "position"], {:members, :"$1", :"$2", :"$3", :"$4", :"$5"})
    [
      {:==, :"$1", 123   }, 
      {:==, :"$4", "foo" } 
   ]
    iex> DbMnesia.wheres_spec([{:==, "name", "hoge"}], ["id", "name", "age", "team", "position"], {:members, :"$1", :"$2", :"$3", :"$4", :"$5"})
    [
      {:==, :"$2", "hoge"} 
   ]
  """
  def wheres_spec(wheres_params, columns, columns_spec) do
    param_only = columns_spec |> Tuple.delete_at(0) |> Tuple.to_list
    wheres_params
    |> Enum.map(fn {op, column, v} -> {op, Enum.at(param_only, Enum.find_index(columns, & &1 == column)), v} end)
  end

  @doc """
  String value to raw value

  ## Examples
    iex> DbMnesia.raw_value("")
    ""
    iex> DbMnesia.raw_value("123")
    123
    iex> DbMnesia.raw_value("12.34")
    12.34
    iex> DbMnesia.raw_value("'hoge'")
    "hoge"
    iex> DbMnesia.raw_value("foo")
    "foo"
    iex> DbMnesia.raw_value("12ab3")
    "12ab3"
  """
  def raw_value(v) when is_binary(v) do
    case String.match?(v, ~r/^([0-9]|\.)+$/) do
      true -> 
        case String.match?(v, ~r/\./) do
          true -> v |> String.to_float
          _    -> v |> String.to_integer
        end
      _ -> v |> String.trim("'") |> String.trim("\"")
    end
  end
  def raw_value(v) when is_number(v), do: v
  def raw_value(v) when is_boolean(v), do: v |> Atom.to_string

  def table_columns(table_atom) do
    :mnesia.table_info(table_atom, :attributes) 
    |> Enum.reduce([], fn(item, acc) -> acc ++ [Atom.to_string(item)] end)
  end 

  def all_columns_param(table_atom) do
    1..Enum.count(table_columns(table_atom))
    |> Enum.reduce({}, fn(x, acc) -> Tuple.append(acc, :"$#{x}")  end)
  end
end
```

# ライブラリ

```elixir:mix.exs
defmodule Basic.MixProject do
…
  defp deps do
    [
      { :smallex, "~> 0.0" },
      …
```