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