Current section
Files
Jump to
Current section
Files
lib/links.ex
defmodule Carve.Links do
@moduledoc """
Carve.Links provides functionality for retrieving and processing links between different entities.
This module is responsible for:
- Fetching links for entities based on their IDs or data structures
- Handling circular references and preventing infinite loops
- Normalizing and preparing the final result set of links
It works in conjunction with the view modules created using Carve.View to generate
a complete set of links for any given entity or set of entities.
"""
require Logger
@doc """
Retrieves links for a given module and ID or list of IDs.
This function handles single IDs, lists of IDs, and prevents circular references
using a visited map.
## Parameters
- `module`: The module to use for fetching and processing links
- `id`: A single ID or a list of IDs
- `visited`: A map to keep track of visited entities (default: %{})
## Returns
A list of prepared link maps.
## Examples
iex> Carve.Links.get_links_by_id(UserJSON, 1)
[%{type: :team, id: "abc123", data: %{...}}, %{type: :profile, id: "def456", data: %{...}}]
iex> Carve.Links.get_links_by_id(UserJSON, [1, 2, 3])
[%{type: :team, id: "abc123", data: %{...}}, %{type: :profile, id: "def456", data: %{...}}, ...]
"""
def get_links_by_id(module, id, visited \\ %{})
def get_links_by_id(_module, nil, _visited), do: []
def get_links_by_id(module, id, visited) when not is_list(id) do
case Map.get(visited, {module, id}) do
nil ->
case module.get_by_id(id) do
nil -> []
data -> get_links_by_data(module, data, visited) |> prepare_result()
end
_ -> []
end
end
def get_links_by_id(module, ids, visited) when is_list(ids) do
Enum.flat_map(ids, &get_links_by_id(module, &1, visited))
|> prepare_result()
end
@doc """
Retrieves links for a given module and data or list of data.
This function handles single data structures, lists of data structures, and
prevents circular references using a visited map.
## Parameters
- `module`: The module to use for fetching and processing links
- `data`: A single data structure or a list of data structures
- `visited`: A map to keep track of visited entities (default: %{})
## Returns
A list of prepared link maps.
## Examples
iex> user = %{id: 1, name: "John Doe", team_id: 2}
iex> Carve.Links.get_links_by_data(UserJSON, user)
[%{type: :team, id: "abc123", data: %{...}}, %{type: :profile, id: "def456", data: %{...}}]
iex> users = [%{id: 1, name: "John"}, %{id: 2, name: "Jane"}]
iex> Carve.Links.get_links_by_data(UserJSON, users)
[%{type: :team, id: "abc123", data: %{...}}, %{type: :profile, id: "def456", data: %{...}}, ...]
"""
def get_links_by_data(module, data, visited \\ %{})
def get_links_by_data(_module, nil, _visited), do: []
def get_links_by_data(module, data_list, visited) when is_list(data_list) do
Enum.flat_map(data_list, &get_links_by_data(module, &1, visited))
|> prepare_result()
end
def get_links_by_data(_module, data, _visited) when not is_map(data) do
[]
end
def get_links_by_data(module, data, visited) when not is_list(data) do
case fetch_id(data) do
{:ok, id} ->
if Map.get(visited, {module, id}) do
[]
else
visited = Map.put(visited, {module, id}, true)
module.process_links(data)
|> Enum.flat_map(fn {link_module, link_ids} ->
Enum.map(normalize_link_ids(link_ids), fn link_id ->
process_single_link(link_module, link_id, visited)
end)
end)
|> prepare_result()
end
:error -> []
end
end
@doc """
Processes a single link, fetching its data and preparing it for output.
This function handles both ID-based and data-based links, preventing circular
references using the visited map.
## Parameters
- `module`: The module to use for fetching and processing the link
- `id_or_data`: Either an ID or a data structure representing the linked entity
- `visited`: A map to keep track of visited entities
## Returns
A prepared link map or nil if the link has already been visited or is invalid.
"""
defp process_single_link(module, id, visited) when is_number(id) or is_binary(id) do
case Map.get(visited, {module, id}) do
nil ->
case module.get_by_id(id) do
nil -> nil
data ->
module.prepare_for_view(data)
end
_ -> nil
end
end
defp process_single_link(module, data, visited) do
case fetch_id(data) do
{:ok, id} ->
case Map.get(visited, {module, id}) do
nil ->
module.prepare_for_view(data)
_ -> nil
end
:error -> nil
end
end
@doc """
Prepares the final result set by flattening, removing nil values, and eliminating duplicates.
## Parameters
- `result`: A list of link maps
## Returns
A cleaned and uniquified list of link maps.
## Examples
iex> result = [[%{type: :team, id: "abc"}, nil], [%{type: :profile, id: "def"}], %{type: :team, id: "abc"}]
iex> Carve.Links.prepare_result(result)
[%{type: :team, id: "abc"}, %{type: :profile, id: "def"}]
"""
def prepare_result(result) do
result
|> List.flatten()
|> Enum.reject(&is_nil/1)
|> Enum.uniq_by(fn %{type: type, id: id} -> {type, id} end)
end
@doc """
Normalizes link IDs to always be a list.
## Parameters
- `link_ids`: A single link ID or a list of link IDs
## Returns
A list of link IDs.
## Examples
iex> Carve.Links.normalize_link_ids(1)
[1]
iex> Carve.Links.normalize_link_ids([1, 2, 3])
[1, 2, 3]
"""
defp normalize_link_ids(link_ids) when is_list(link_ids), do: link_ids
defp normalize_link_ids(link_id), do: [link_id]
@doc """
Fetches the ID from a data structure.
This function attempts to find an ID in a map using various common key formats.
## Parameters
- `data`: A map or other data structure potentially containing an ID
## Returns
`{:ok, id}` if an ID is found, `:error` otherwise.
## Examples
iex> Carve.Links.fetch_id(%{id: 1})
{:ok, 1}
iex> Carve.Links.fetch_id(%{"id" => 2})
{:ok, 2}
iex> Carve.Links.fetch_id(%{foo: "bar"})
{:ok, {:foo, "bar"}}
iex> Carve.Links.fetch_id("not a map")
:error
"""
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