lib/quarto/cursor_value.ex

defmodule Quarto.CursorValue do
  @moduledoc """
  Module called to build the cursor

  Receives an entry, this will most likely be an Ecto struct.
  From the config it will derive the cursor fields and will fetch the value
  of those fields from the struct. It tries to chase down bindings to see if
  it can find the cursor field on related records.
  """
  @spec build(any, Quarto.Config.t()) :: [any]
  def build(entry, %{cursor_fields: cursor_fields, queryable: queryable}) do
    Enum.map(cursor_fields, &get_cursor_value(&1, entry, queryable))
  end

  defp get_cursor_value({field, {0, _}}, entry, _queryable) do
    Map.get(entry, field)
  end

  defp get_cursor_value({field, {position, _}}, entry, queryable) do
    case chase_binding(queryable, position) do
      nil ->
        raise "Could not find a binding in position #{position} in assocs #{
                inspect(queryable.assocs)
              } or in aliases #{inspect(queryable.aliases)}"

      key ->
        key = key |> Enum.reverse() |> Enum.map(&Access.key/1)
        schema = get_in(entry, key)
        Map.get(schema, field)
    end
  end

  defp chase_binding(queryable, position) do
    case find_alias_binding(queryable, position) do
      nil -> find_assoc(queryable.assocs, position)
      alias_binding -> alias_binding
    end
  end

  # First check aliases to see if matching binding
  defp find_alias_binding(queryable, position) do
    Enum.find_value(queryable.aliases, fn
      {name, ^position} -> [name]
      _ -> false
    end)
  end

  # the assocs on the queryable can also be inspected
  # for a binding in the right position
  defp find_assoc(assocs, position) do
    Enum.find_value(assocs, fn
      {key, {^position, _}} -> [key]
      {key, {_, assocs}} -> find_assoc(assocs, position) ++ [key]
      _ -> false
    end)
  end
end