フリーランスエンジニアのブログ

Elixir,Kubernetes,Terraformをいじっているエンジニアのブログ。メモ帳

RubyプロダクトをElixirプロダクトへリプレイスする作業を自動化しよう① fukuoka.ex Elixir/Phoenix Advent Calendar 2020【5分で読める】

この投稿は『fukuoka.ex Elixir/Phoenix Advent Calendar 2020』の19日目の記事です。
qiita.com


また、この投稿は、先日のElixir Digitalization Implementors #3 でLT発表させてもらった内容と一部重複しています。
fukuokaex.connpass.com

Schemaに関わる実装は自動化しよう。

RubyプロダクトのSchemaを把握してElixirで実装する作業って自動化できるのでは?

そう考えてこれを作りました。

github.com
f:id:ruby-deve:20201219225313p:plain

解説

前述のrtoeは既存のRubyプロダクトのmigrationファイルを読み込み、
ElixirプロダクトでSchemaを生成するコマンドを生成するものです。

一部抜粋して解説します。
基本的にこんなことをやっています。

  1. Rubyプロダクトのmigrationファイルを読み込む
  2. 読み込むんだ下記の文字列を加工

これがメインとなるメソッドです。

defmodule Mix.Tasks.Schema do
  @moduledoc "Printed when the user requests `mix schema migration_file_path`"
  @shortdoc "Migration file argument"

  use Mix.Task

  @impl Mix.Task
  def run(file) do
    {:ok, contents} = File.read(file)

    lines = String.split(contents, "\n")

    table =
      lines
      |> get_table_name

    class =
      lines
      |> get_class_name

    columns =
      lines
      |> get_columns

    elements = [
      "mix phx.gen.schema Context.#{class}",
      "context_#{table}",
      columns
    ]

    Mix.shell().info(Enum.join(elements, " "))
  end

  defp get_table_name(lines) do
    line =
      lines
      |> line_containing_word("create_table")

    case Regex.named_captures(~r/^create_table :(?<tn>.*) do |t|$/, "#{line}") do
      nil ->
        ""

      %{"tn" => table_name} ->
        table_name
    end
  end

  defp get_class_name(lines) do
    line =
      lines
      |> line_containing_word("class Create")

    case Regex.named_captures(~r/^class Create(?<cn>.*)s < ActiveRecord::Migration$/, "#{line}") do
      nil ->
        ""

      %{"cn" => class_name} ->
        class_name
    end
  end

  defp get_columns(lines) do
    lines
    |> Enum.filter(&is_column(String.trim("#{&1}")))
    |> Enum.map(fn str -> String.trim(str) end)
    |> Enum.map(fn str -> get_column_type(str) end)
  end

  defp get_column_type(str) do
    lc = String.split(str, ", ")
    [head | _] = lc

    case Regex.named_captures(~r/^t.(?<type>.*) :(?<col>.*)$/, "#{head}") do
      nil ->
        ""

      %{"type" => type, "col" => col} ->
        "#{col}:#{type} "
    end
  end

  defp is_column(line) do
    case Regex.named_captures(~r/^t.*$/, "#{line}") do
      nil ->
        false

      %{} ->
        true
    end
  end

  def line_containing_word(lines, word) do
    lines
    |> Enum.find(&String.contains?("#{&1}", word))
    |> String.trim()
  end
end

最後に適切なmix phx.gen.schemaコマンドを生成してくれます。

実際に

プロジェクトでも使っていて、
手作業でElixirのSchemaを実装する場合に比べてカラム名や型のミスもなくなりました。

今後

rtoeに関して、Elixir Digitalization Implementors #3 で頂いた下記のフィードバックも今後やりたいです。

  • CLI化 →releaseしました。(2020/12/28更新)

github.com

  • ディレクトリを指定して全てのRubyプロダクトのmigrationファイルを読み込み、全てのmix gen.schemaコマンドを生成する

また、Elixirへのリプレイスに関してどんなプロジェクトでも行う共通の工程があると思うのですが、できればそこを自動化していきたいです。


ちょっと短めですが、失礼いたします。