Current section
Files
Jump to
Current section
Files
lib/spec_db/src_helpers.ex
defmodule Sorcery.SpecDb.SrcHelpers do
@moduledoc """
Generates and types a %Src{} that matches the given @src_table. Makes working with interceptors much easier.
```elixir
defmodule MyInterceptor do
@src_table %{
args: %{
dog_id: %{t: :id, placeholder: "$sorcery:dog:1"},
kennel_id: %{t: :id, placeholder: "$sorcery:kennel:1"},
},
db: %{
dog: %{
"$sorcery:dog:1" => {Dog, %{kennel_id: "$sorcery:kennel:1"}}
},
kennel: %{
"$sorcery:kennel:1" => {Kennel, %{name: "Test Kennel"}}
},
}
}
require Sorcery.SpecDb.SrcHelpers
Sorcery.SpecDb.SrcHelpers.build_interceptor()
end
# This assumes the Dog and Kennel modules both, also, implement t/0 and gen/1
# Now we can call:
iex> [src] = MyInterceptor.gen() |> Enum.take(1)
iex> Norm.valid?(src, MyInterceptor.t())
true
```
"""
use Norm
alias Sorcery.SpecDb.{NormHelpers}
def any?(), do: spec(fn _ -> true end)
@doc """
Extracts placeholder ids from @src_table, and finds all the paths that reference each placeholder
"""
def get_placeholder_data(src_table) do
acc = %{}
acc = Enum.reduce(src_table.args, acc, fn
{k, %{t: :id, placeholder: "$sorcery:" <> _ = id}}, acc ->
Map.update(acc, id, %{arg_paths: [k], db_paths: []}, fn %{arg_paths: arg_paths} = old ->
Map.put(old, :arg_paths, [k | arg_paths])
end)
{_k, _v}, acc -> acc
end)
acc = Enum.reduce(src_table.db, acc, fn {tk, table}, acc ->
Enum.reduce(table, acc, fn {id, {_mod, entity}}, acc ->
# Get any placeholder ids directly at db.tk.id level
acc = case id do
"$sorcery:" <> _ ->
Map.update(acc, id, %{arg_paths: [], db_paths: [[tk]]}, fn %{db_paths: db_paths} = old ->
Map.put(old, :db_paths, [[tk] | db_paths])
end)
_ -> acc
end
# Now get placeholders in arbitrary fields
acc = Enum.reduce(entity, acc, fn
{k, "$sorcery:" <> _ = phid}, acc ->
Map.update(acc, phid, %{arg_paths: [], db_paths: [[tk, id, k]]}, fn %{db_paths: db_paths} = old ->
Map.put(old, :db_paths, [[tk, id, k] | db_paths])
end)
_, acc -> acc
end)
acc
end)
end)
acc
end
@doc """
For each placeholder, generate a unique integer for it
"""
def realize_placeholders(placeholder_data) do
n = placeholder_data |> Map.keys() |> Enum.count()
ids = Enum.take(StreamData.uniq_list_of(StreamData.integer(0..99999), length: 1), n)
Enum.with_index(placeholder_data)
|> Enum.reduce(placeholder_data, fn {{phid, _data}, idx}, acc ->
[id] = Enum.at(ids, idx)
put_in(acc, [phid, :real], id)
end)
end
defmacro build_interceptor() do
quote do
def src_table, do: @src_table
def gen() do
placeholders = Sorcery.SpecDb.SrcHelpers.get_placeholder_data(@src_table)
|> Sorcery.SpecDb.SrcHelpers.realize_placeholders()
args_map = Sorcery.SpecDb.SrcHelpers.gen_args(@src_table, placeholders) |> StreamData.fixed_map()
db_map = Sorcery.SpecDb.SrcHelpers.gen_db(@src_table, placeholders) |> StreamData.fixed_map()
StreamData.fixed_map(%{args: args_map, original_db: db_map})
|> StreamData.map(fn m -> struct(Sorcery.Src, m) end)
end
def t() do
# Dear Future, I am so sorry.
arg_schema = NormHelpers.build_schema(Map.get(@src_table, :args, %{}))
db_schema = Map.get(@src_table, :db, %{})
|> Enum.reduce(%{}, fn {tk, table}, acc ->
table_schema = spec(is_map() and fn entities ->
Enum.all?(table, fn {_id, {mod, _args}} ->
Enum.any?(entities, fn {_, entity} ->
Norm.valid?(entity, mod.t())
end)
end)
end)
Map.put(acc, tk, table_schema)
end)
|> Norm.schema()
|> selection(Map.keys(Map.get(@src_table, :db, %{})))
schema(%{
args: arg_schema,
original_db: db_schema
})
end
end
end
def gen_args(src_table, placeholders) do
arg_spec = Map.get(src_table, :args, %{})
Enum.reduce(arg_spec, %{}, fn
{k, %{t: :id, placeholder: "$sorcery:" <> _ = phid}}, acc ->
id = Map.get(placeholders, phid).real
Map.put(acc, k, StreamData.constant(id))
{k, v}, acc ->
Map.put(acc, k, StreamData.constant(v))
end)
end
def gen_db(src_table, placeholders) do
db_spec = Map.get(src_table, :db, %{})
Enum.reduce(db_spec, %{}, fn {tk, table}, acc ->
new_table = gen_table(table, placeholders)
Map.put(acc, tk, new_table)
end)
end
def gen_table(table, placeholders) do
Enum.reduce(table, %{}, fn
{"$sorcery:" <> _ = phid, {mod, args}}, acc ->
id = Map.get(placeholders, phid).real
args = Map.put(args, :id, id)
|> realize_args(placeholders)
entity = mod.gen(args)
Map.put(acc, id, entity)
{id, {mod, args}}, acc ->
args = realize_args(args, placeholders)
entity = mod.gen(args)
Map.put(acc, id, entity)
end)
|> StreamData.fixed_map()
end
def realize_args(args, placeholders) do
Enum.reduce(args, %{}, fn
{k, "$sorcery:" <> _ = phid}, acc ->
id = Map.get(placeholders, phid).real
Map.put(acc, k, id)
{k, v}, acc -> Map.put(acc, k, v)
end)
end
end