Packages

DSL for building JSON APIs fast. Creates endpoint views, renders linked data automatically.

Current section

Files

Jump to
carve lib links.ex
Raw

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. It also supports filtering links based on a whitelist.
## 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: %{})
- `whitelist`: An optional list of allowed link types (default: nil)
## 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], %{}, [:team])
[%{type: :team, id: "abc123", data: %{...}}]
"""
def get_links_by_id(module, id, visited \\ %{}, whitelist \\ nil)
def get_links_by_id(_module, nil, _visited, _whitelist), do: []
def get_links_by_id(module, id, visited, whitelist) 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, whitelist) |> prepare_result(whitelist)
end
_ ->
[]
end
end
def get_links_by_id(module, ids, visited, whitelist) when is_list(ids) do
Enum.flat_map(ids, &get_links_by_id(module, &1, visited, whitelist))
|> prepare_result(whitelist)
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. It also supports filtering links based on a whitelist.
## 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: %{})
- `whitelist`: An optional list of allowed link types (default: nil)
## 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> user = %{id: 1, name: "John Doe", team_id: 2}
iex> Carve.Links.get_links_by_data(UserJSON, user, %{}, [:team])
[%{type: :team, id: "abc123", data: %{...}}]
"""
# Get links for an entity by its data, with support for collections and filtering
def get_links_by_data(module, data, visited \\ %{}, whitelist \\ nil)
# Return empty list for nil data
def get_links_by_data(_module, nil, _visited, _whitelist), do: []
# Handle collections by mapping over each item
def get_links_by_data(module, data_list, visited, whitelist) when is_list(data_list) do
Enum.flat_map(data_list, &get_links_by_data(module, &1, visited, whitelist))
|> prepare_result(whitelist)
end
# Return empty list for non-map data
def get_links_by_data(_module, data, _visited, _whitelist) when not is_map(data), do: []
# Process single entity's links, with circular reference prevention
def get_links_by_data(module, data, visited, whitelist) when not is_list(data) do
case fetch_id(data) do
{:ok, id} ->
if Map.get(visited, {module, id}) do
[]
else
# Mark as visited and process links
visited = Map.put(visited, {module, id}, true)
# Get raw links and apply whitelist filtering/evaluation
links =
module.declare_links(data)
|> filter_and_evaluate_links(whitelist)
links =
links
|> Enum.flat_map(fn {link_module, link_ids} ->
link_ids
|> normalize_link_ids()
|> Enum.map(fn link_id ->
link = process_single_link(link_module, link_id, visited)
children = if Map.get(visited, {link_module, link_id}) do
[]
else
if is_map(link_id) do
get_links_by_data(link_module, link_id, visited, whitelist)
else
get_links_by_id(link_module, link_id, visited, whitelist)
end
end
[link | children]
end)
end)
prepare_result(links, whitelist)
end
:error ->
[]
end
end
defp filter_and_evaluate_links(links, nil) do
# Without whitelist - evaluate regular links only (no lazy)
links
|> Enum.filter(fn {_, value} -> not is_function(value) end)
|> Enum.into(%{})
end
defp filter_and_evaluate_links(_links, []) do
# Empty whitelist - evaluate nothing
%{}
end
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
@doc """
Filters links based on the provided whitelist.
## Parameters
- `links`: A map of links where keys are modules and values are IDs or lists of IDs
- `whitelist`: A list of allowed link types (atoms)
## Returns
A filtered map of links.
## Examples
iex> links = %{UserJSON => [1, 2], PostJSON => [3, 4]}
iex> Carve.Links.filter_links(links, [:user])
%{UserJSON => [1, 2]}
"""
defp filter_links(links, nil), do: links
defp filter_links(links, whitelist) do
links
|> Enum.filter(fn {module, _} -> module.type_name() in whitelist end)
|> Enum.into(%{})
end
@doc """
Prepares the final result set by flattening, removing nil values, eliminating duplicates,
and applying the whitelist filter if provided.
## Parameters
- `result`: A list of link maps
- `whitelist`: An optional list of allowed link types
## Returns
A cleaned, uniquified, and optionally filtered 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, [:team])
[%{type: :team, id: "abc"}]
"""
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
@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 """
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