Skip to content

Instantly share code, notes, and snippets.

@frekw
Created October 12, 2017 15:13
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save frekw/85e0a12b60bd77bc43a1f6fc4e08a9d1 to your computer and use it in GitHub Desktop.
Save frekw/85e0a12b60bd77bc43a1f6fc4e08a9d1 to your computer and use it in GitHub Desktop.
defmodule Forma do
# defmacro __using__([]) do
# quote do
# import Forma, only: [forma: 2]
# end
# end
# defmacro forma(name, module) do
# module = Macro.expand(module, __CALLER__)
# transform = Forma.compile_module(module)
# quote do
# def unquote(name)(input) do
# unquote(transform).(input)
# end
# end
# end
def compile_module(mod) do
mod
|> specs_for()
end
def into(data, module) do
module
|> compile_module()
|> build(data)
end
def build(parser, data) do
parser.(data)
end
defp specs_for(mod) do
types = types_table(mod)
compile(types, get_type(types, :t))
end
defp types_table(mod) do
mod
|> Kernel.Typespec.beam_types()
|> Enum.reduce(%{}, fn {:type, {name, definition, _}}, acc ->
Map.put(acc, name, definition)
end)
end
defp get_type(types, type) do
Map.get(types, type)
end
defp exact_map(types, tree) do
transforms = Enum.map(tree, &compile(types, &1))
fn x ->
Enum.reduce(transforms, %{}, fn (transform, acc) ->
{k, v} = transform.(x)
Map.put(acc, k, v)
end)
end
end
defp assoc_map(types, field) do
transform = compile(types, field)
fn xs ->
Enum.reduce(xs, %{}, fn (x, acc) ->
{k, v} = transform.(x)
Map.put(acc, k, v)
end)
end
end
# flatten fields
defp compile(types, [x]), do: compile(types, x)
defp compile(types, {:type, _, :map, tree}) when is_list(tree) do
case tree do
[{:type, _, :map_field_exact, _} | _] -> exact_map(types, tree)
[{:type, _, :map_field_assoc, _} = field | _] -> assoc_map(types, field)
end
end
defp compile(types, {:type, _, :map_field_assoc, [key, val]}) do
key = compile(types, key)
val = compile(types, val)
fn {k, v} ->
{key.(k), val.(v)}
end
end
# special __struct__ field
defp compile(_types, {:type, _, :map_field_exact, [{:atom, _, :__struct__}, {:atom, _, name}]}) do
fn _ ->
{:__struct__, name}
end
end
defp compile(types, {:type, _, :map_field_exact, [{:atom, _, name}, field]}) do
fn x ->
raw = Map.get(x, to_string(name))
value = compile(types, field).(raw)
{name, value}
end
end
defp compile(types, {:type, _, :list, children}) do
transform = compile(types, children)
fn x -> Enum.map(x, transform) end
end
# {:remote_type, 103, [{:atom, 0, String}, {:atom, 0, :t}, []]}
defp compile(types, {:remote_type, _, [{:atom, _, module}, {:atom, _, type}, _]}) do
all = types_table(module)
compile(types, Map.get(all, type))
end
defp compile(types, {:user_type, _, type, _}) do
compile(types, Map.get(types, type))
end
defp compile(_types, {:type, _, :boolean, _}) do
fn
true -> true
false -> false
end
end
defp compile(types, {:type, _, :union, possible}) do
fn x ->
Enum.find_value(possible, fn candidate ->
try do
compile(types, candidate).(x)
rescue
_ -> false
end
end)
end
end
defp compile(_types, {:type, _, :binary, _}) do
fn
x when is_binary(x) -> x
nil -> nil
end
end
defp compile(_, {:type, _, :integer, _}) do
fn
x when is_number(x) -> trunc(x)
end
end
defp compile(_, {:type, _, :float, _}) do
fn
x when is_number(x) -> x
end
end
defp compile(_types, {:type, _, :atom, []}) do
fn
x when is_atom(x) -> x
x when is_binary(x) -> String.to_atom(x)
end
end
defp compile(_, {:atom, _, value}) do
string = Atom.to_string(value)
fn
^value -> value
^string -> value
nil -> nil
end
end
defp compile(_types, _field) do
fn _ -> raise "unmapped" end
end
end
defmodule Forma.Test do
defstruct name: "",
numbers: [],
floats: [],
stuff: %{},
label: nil,
atom: nil,
bool: nil
@type t :: %__MODULE__{
name: String.t,
numbers: [integer()],
floats: [float()],
stuff: %{
optional(atom) => String.t,
},
atom: atom,
label: foo,
bool: boolean
}
@type foo :: :foo | :bar
@input %{
"name" => "Name",
"numbers" => [1, 2, 3, 4, 5.2],
"floats" => [1.1, 2.3, 3],
"stuff" => %{"key" => "value", "bar" => "value"},
"label" => "foo",
"atom" => "hello",
"bool" => true
}
def run do
Forma.into(@input, __MODULE__)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment