Skip to content

Instantly share code, notes, and snippets.

@Bestra
Created April 24, 2019 12:10
Show Gist options
  • Save Bestra/92ac7c0e6d52405f9edfb393b8e25e67 to your computer and use it in GitHub Desktop.
Save Bestra/92ac7c0e6d52405f9edfb393b8e25e67 to your computer and use it in GitHub Desktop.
Parse a schema.rb file and make ecto schemas from it
defmodule Mix.Tasks.CreateSchemaFromRails do
use Mix.Task
defmodule EctoPrinter do
def build_string(module_name, %{name: table_name, body: body}) do
model_name =
table_name
|> String.trim_trailing("s")
|> Macro.camelize()
fields = build_fields(body)
[
"defmodule #{module_name}.#{model_name} do",
"use Ecto.Schema",
"import Ecto.Changeset",
"",
"schema \"#{table_name}\" do",
fields,
"",
"timestamps(inserted_at: :created_at)",
"end",
"end",
""
]
|> List.flatten()
end
def build_fields(body) do
Enum.map(body, fn column_def ->
%{column_name: column_name, column_type: column_type} = column_def
"field :#{column_name}, :#{ecto_type(column_type)}"
end)
end
def ecto_type(column_type) do
case column_type do
"bigint" -> :integer
"boolean" -> :boolean
"citext" -> :string
"date" -> :date
"datetime" -> :utc_datetime
"decimal" -> :decimal
"inet" -> :string
"integer" -> :integer
"json" -> :map
"jsonb" -> :map
"string" -> :string
"text" -> :string
end
end
end
defmodule SchemaParser.Helpers do
import NimbleParsec
def find_next(s) do
repeat_until(empty(), ignore(utf8_char([])), [string(s)])
end
def capture_until(s) do
repeat_until(empty(), utf8_char([]), [string(s)])
end
end
defmodule SchemaParser do
import NimbleParsec
import SchemaParser.Helpers
string_literal =
ignore(ascii_char([?"]))
|> times(ascii_char([{:not, ?"}]), min: 1)
|> ignore(ascii_char([?"]))
column_spec =
find_next("t.")
|> ignore(string("t."))
|> tag(ascii_string([?a..?z, ?_], min: 1), :column_type)
|> ignore(string(" "))
|> tag(string_literal, :column_name)
|> traverse({:process_column, []})
table_name =
ignore(string("create_table "))
|> tag(string_literal, :table_name)
table_def =
find_next("create_table")
|> concat(table_name)
|> concat(find_next("t."))
|> concat(tag(capture_until("end\n"), :body))
|> traverse({:process_table_body, []})
defparsec(:parse_tables, times(table_def, min: 1))
defparsec(:parse_column, column_spec)
defp process_parsed_column(s) when is_binary(s) do
case parse_column(s) do
{:ok, [res], _rest, _, _, _} ->
res
{:error, message, _, _, _, _} ->
IO.puts(message)
IO.inspect(s, label: "Original string")
end
end
defp process_column(_rest, args, context, _line, _offset) do
column_name = args[:column_name]
[column_type] = args[:column_type]
{%{column_name: column_name, column_type: column_type} |> List.wrap(), context}
end
defp process_table_body(_rest, args, context, _line, _offset) do
body =
args[:body]
|> String.Chars.to_string()
|> String.split("\n")
|> Enum.reject(&String.contains?(&1, "t.index"))
|> Enum.reject(&String.contains?(&1, "\"created_at\""))
|> Enum.reject(&String.contains?(&1, "\"updated_at\""))
|> Enum.reject(&(String.trim(&1) == ""))
|> Enum.map(&process_parsed_column(&1))
ast = %{body: body, name: args[:table_name] |> String.Chars.to_string()}
{ast |> List.wrap(), context}
end
end
@shortdoc "Does stuff"
def run(args) do
{:ok, tables, _, _, _, _} = schema_file() |> SchemaParser.parse_tables()
# column_types =
# Enum.flat_map(tables, & &1[:body])
# |> Enum.reduce(MapSet.new(), fn column, set ->
# MapSet.put(set, column[:column_type])
# end)
# IO.inspect(column_types)
module_name = args[0]
models = Enum.map(tables, &EctoPrinter.build_string(module_name, &1))
Enum.each(
models,
&(Enum.join(&1, "\n")
|> Code.format_string!()
|> IO.puts())
)
end
def run(), do: run(true)
def schema_file do
Path.expand("/path/to/schema") |> File.read!()
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment