Created
April 24, 2019 12:10
-
-
Save Bestra/92ac7c0e6d52405f9edfb393b8e25e67 to your computer and use it in GitHub Desktop.
Parse a schema.rb file and make ecto schemas from it
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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