Current section
Files
Jump to
Current section
Files
lib/links.ex
defmodule Carve.Links do
# Public API — returns lists (backward compatible)
def get_links_by_id(module, id, visited \\ %{}, whitelist \\ nil, cache_key \\ nil) do
{links, _visited} = do_get_links_by_id(module, id, visited, whitelist, cache_key)
links
end
def get_links_by_data(module, data, visited \\ %{}, whitelist \\ nil, cache_key \\ nil) do
{links, _visited} = do_get_links_by_data(module, data, visited, whitelist, cache_key)
links
end
# Internal API — returns {links, visited} to accumulate visited across items
defp do_get_links_by_id(_module, nil, visited, _whitelist, _cache_key), do: {[], visited}
defp do_get_links_by_id(module, id, visited, whitelist, cache_key) when not is_list(id) do
cache_key = cache_key || Carve.Cache.get_or_create_context()
case Map.get(visited, {module, id}) do
nil ->
data = Carve.Cache.fetch(cache_key, {module, :get, id}, fn ->
module.get_by_id(id)
end)
case data do
nil ->
{[], visited}
data ->
{links, visited} = do_get_links_by_data(module, data, visited, whitelist, cache_key)
{prepare_result(links, whitelist), visited}
end
_ ->
{[], visited}
end
end
defp do_get_links_by_id(module, ids, visited, whitelist, cache_key) when is_list(ids) do
cache_key = cache_key || Carve.Cache.get_or_create_context()
{links, visited} =
Enum.reduce(ids, {[], visited}, fn id, {acc, vis} ->
{new_links, vis} = do_get_links_by_id(module, id, vis, whitelist, cache_key)
{acc ++ new_links, vis}
end)
{prepare_result(links, whitelist), visited}
end
defp do_get_links_by_data(_module, nil, visited, _whitelist, _cache_key), do: {[], visited}
defp do_get_links_by_data(module, data_list, visited, whitelist, cache_key)
when is_list(data_list) do
cache_key = cache_key || Carve.Cache.get_or_create_context()
{links, visited} =
Enum.reduce(data_list, {[], visited}, fn item, {acc, vis} ->
{new_links, vis} = do_get_links_by_data(module, item, vis, whitelist, cache_key)
{acc ++ new_links, vis}
end)
{prepare_result(links, whitelist), visited}
end
defp do_get_links_by_data(_module, data, visited, _whitelist, _cache_key)
when not is_map(data),
do: {[], visited}
defp do_get_links_by_data(module, data, visited, whitelist, cache_key)
when not is_list(data) do
cache_key = cache_key || Carve.Cache.get_or_create_context()
case fetch_id(data) do
{:ok, id} ->
if Map.get(visited, {module, id}) do
{[], visited}
else
visited = Map.put(visited, {module, id}, true)
cached = fetch_cached(module, data, id, cache_key)
links =
module.declare_links(data, cached)
|> filter_and_evaluate_links(whitelist)
{links, visited} =
Enum.reduce(links, {[], visited}, fn {link_module, link_data_or_ids}, {acc, vis} ->
link_data_or_ids
|> normalize_link_ids()
|> Enum.reduce({acc, vis}, fn link_id_or_data, {acc, vis} ->
link = process_single_link(link_module, link_id_or_data, vis, cache_key)
# let do_get_links_by_id/data mark visited internally
{children, vis} =
if is_map(link_id_or_data) do
do_get_links_by_data(link_module, link_id_or_data, vis, whitelist, cache_key)
else
do_get_links_by_id(link_module, link_id_or_data, vis, whitelist, cache_key)
end
{acc ++ [link | children], vis}
end)
end)
{prepare_result(links, whitelist), visited}
end
:error ->
{[], visited}
end
end
# Fetch cached data for a module, using the Cachex cache
defp fetch_cached(module, data, id, cache_key) do
Carve.Cache.fetch(cache_key, {module, :cache, id}, fn ->
module.__cache__(data)
end)
end
defp process_single_link(module, id, visited, cache_key) when is_number(id) or is_binary(id) do
case Map.get(visited, {module, id}) do
nil ->
data = Carve.Cache.fetch(cache_key, {module, :get, id}, fn ->
module.get_by_id(id)
end)
case data do
nil -> nil
data ->
cached = fetch_cached(module, data, id, cache_key)
module.prepare_for_view(data, cached)
end
_ ->
nil
end
end
defp process_single_link(module, data, visited, cache_key) do
case fetch_id(data) do
{:ok, id} ->
case Map.get(visited, {module, id}) do
nil ->
cached = fetch_cached(module, data, id, cache_key)
module.prepare_for_view(data, cached)
_ ->
nil
end
:error ->
nil
end
end
defp filter_and_evaluate_links(links, nil) do
links
|> Enum.filter(fn {_, value} -> not is_function(value) end)
|> Enum.into(%{})
end
defp filter_and_evaluate_links(_links, []), do: %{}
defp filter_and_evaluate_links(links, whitelist) when is_list(whitelist) do
links
|> Enum.filter(fn {module, _} -> module.type_name() in whitelist end)
|> Enum.map(fn {module, value} ->
evaluated =
case value do
fun when is_function(fun, 0) -> fun.()
other -> other
end
{module, evaluated}
end)
|> Enum.into(%{})
end
def prepare_result(result, whitelist \\ nil) do
result
|> List.flatten()
|> Enum.reject(&is_nil/1)
|> Enum.uniq_by(fn %{type: type, id: id} -> {type, id} end)
|> filter_result(whitelist)
end
defp filter_result(_result, []), do: []
defp filter_result(result, nil), do: result
defp filter_result(result, whitelist) do
Enum.filter(result, fn %{type: type} -> type in whitelist end)
end
defp extract_id(nil), do: nil
defp extract_id(id) when is_number(id) or is_binary(id), do: id
defp extract_id(data) when is_map(data) do
case fetch_id(data) do
{:ok, id} -> id
:error -> nil
end
end
defp normalize_link_ids(link_ids) when is_list(link_ids), do: link_ids
defp normalize_link_ids(link_id), do: [link_id]
defp fetch_id(data) when is_map(data) do
cond do
Map.has_key?(data, :id) -> {:ok, data.id}
Map.has_key?(data, "id") -> {:ok, data["id"]}
true ->
case Enum.at(data, 0) do
{key, value} -> {:ok, {key, value}}
nil -> :error
end
end
end
defp fetch_id(id) when is_integer(id) or is_binary(id), do: {:ok, id}
defp fetch_id(_), do: :error
end