Current section

Files

Jump to
speck lib mix tasks compiler.ex
Raw

lib/mix/tasks/compiler.ex

defmodule Mix.Tasks.Compile.Speck do
@moduledoc """
Compiles Speck schemas
"""
use Mix.Task.Compiler
@schema_path Application.compile_env(:speck, :schema_path, "protocol")
@manifest_path Path.join(Mix.Project.app_path, "speck.manifest")
@impl Mix.Task.Compiler
def run(_args) do
files = Path.wildcard("#{@schema_path}/**/*.ex")
hashes = hashes(files)
manifest = load_manifest()
modified_files =
hashes
|> Enum.reject(&Enum.member?(manifest, &1))
|> Enum.map(fn {_hash, file} -> file end)
Enum.each(modified_files, fn file ->
Mix.Shell.IO.info "Compiling #{file}"
compile(file)
|> Enum.each(fn {module, bytecode} ->
path = Path.join([Mix.Project.app_path, "ebin", "#{module}.beam"])
File.mkdir_p(Path.dirname(path))
File.write(path, bytecode)
end)
end)
files
|> hashes()
|> save_manifest()
:ok
end
@impl Mix.Task.Compiler
def manifests do
[@manifest_path]
end
@impl Mix.Task.Compiler
def clean do
File.rm_rf(@manifest_path)
end
defp compile(file) do
file_ast =
file
|> File.read!
|> Code.string_to_quoted!
{:__block__, _, top_ast} = file_ast
context = Enum.reduce(top_ast, %{attributes: []}, fn
{:struct, _, [{:__aliases__, _, _namespace} = module_ast]}, acc ->
{module, _} = Code.eval_quoted(module_ast)
Map.put(acc, :module, module)
{:name, _, [name]}, acc ->
Map.put(acc, :name, name)
{:attribute, _, _} = attribute_ast, acc ->
attributes = acc.attributes ++ [build_attribute(attribute_ast)]
Map.put(acc, :attributes, attributes)
_, acc ->
acc
end)
top_level_attribute_names = Enum.map(context.attributes, &elem(&1, 0))
module_ast =
quote do
defmodule unquote(Macro.escape(context.module)) do
@moduledoc false
defstruct unquote(Macro.escape(top_level_attribute_names))
def attributes, do: unquote(Macro.escape(context.attributes))
end
end
Code.compile_quoted(module_ast, file)
end
defp build_attribute({:attribute, _, [[name], opts_ast, [do: {:__block__, _, attributes_ast}]]}) do
{opts, _} = Code.eval_quoted(opts_ast)
{name, [:map], opts, Enum.map(attributes_ast, &build_attribute/1)}
end
defp build_attribute({:attribute, _, [[name], [do: {:__block__, _, attributes_ast}]]}) do
{name, [:map], [], Enum.map(attributes_ast, &build_attribute/1)}
end
defp build_attribute({:attribute, _, [name, opts_ast, [do: {:__block__, _, attributes_ast}]]}) do
{opts, _} = Code.eval_quoted(opts_ast)
{name, :map, opts, Enum.map(attributes_ast, &build_attribute/1)}
end
defp build_attribute({:attribute, _, [name, [do: {:__block__, _, attributes_ast}]]}) do
{name, :map, [], Enum.map(attributes_ast, &build_attribute/1)}
end
defp build_attribute({:attribute, _, [name, [do: attributes_ast]]}) do
{name, :map, [], [build_attribute(attributes_ast)]}
end
defp build_attribute({:attribute, _, [name, type]}) do
{name, type, []}
end
defp build_attribute({:attribute, _, [name, type, opts_ast]}) do
{opts, _} = Code.eval_quoted(opts_ast)
{name, type, opts}
end
defp hashes(files) when is_list(files) do
Enum.map(files, fn file ->
{hash(file), file}
end)
end
defp hash(file) when is_binary(file) do
contents = File.read!(file)
:crypto.hash(:sha256, contents)
|> Base.encode16
|> String.downcase
end
defp load_manifest do
case File.read(@manifest_path) do
{:ok, contents} -> :erlang.binary_to_term(contents)
{:error, :enoent} -> []
{:error, reason} -> raise "Failed to load manifest: #{reason}"
end
end
defp save_manifest(hashes) do
File.write!(@manifest_path, :erlang.term_to_binary(hashes))
end
end