Packages
phoenix_kit
1.7.170
1.7.208
1.7.207
1.7.206
1.7.205
1.7.204
1.7.203
1.7.202
1.7.201
1.7.200
1.7.199
1.7.198
1.7.197
1.7.196
1.7.194
1.7.193
1.7.192
1.7.191
1.7.190
1.7.189
1.7.187
1.7.186
1.7.185
1.7.184
1.7.183
1.7.182
1.7.181
1.7.180
1.7.179
1.7.178
1.7.177
1.7.176
1.7.175
1.7.174
1.7.173
1.7.172
1.7.171
1.7.170
1.7.169
1.7.168
1.7.167
1.7.166
1.7.165
1.7.164
1.7.162
1.7.161
1.7.160
1.7.159
1.7.157
1.7.156
1.7.155
1.7.154
1.7.153
1.7.152
1.7.151
1.7.150
1.7.149
1.7.146
1.7.145
1.7.144
1.7.143
1.7.138
1.7.133
1.7.132
1.7.131
1.7.130
1.7.128
1.7.126
1.7.125
1.7.121
1.7.120
1.7.119
1.7.118
1.7.117
1.7.116
1.7.115
1.7.114
1.7.113
1.7.112
1.7.111
1.7.110
1.7.109
1.7.108
1.7.107
1.7.106
1.7.105
1.7.104
1.7.103
1.7.102
1.7.101
1.7.100
1.7.99
1.7.98
1.7.97
1.7.96
1.7.95
1.7.94
1.7.93
1.7.92
1.7.91
1.7.90
1.7.89
1.7.88
1.7.87
1.7.86
1.7.85
1.7.84
1.7.83
1.7.82
1.7.81
1.7.80
1.7.79
1.7.78
1.7.77
1.7.76
1.7.75
1.7.74
1.7.71
1.7.70
1.7.69
1.7.66
1.7.65
1.7.64
1.7.63
1.7.62
1.7.61
1.7.59
1.7.58
1.7.57
1.7.56
1.7.55
1.7.54
1.7.53
1.7.52
1.7.51
1.7.49
1.7.44
1.7.43
1.7.42
1.7.41
1.7.39
1.7.38
1.7.37
1.7.36
1.7.34
1.7.33
1.7.31
1.7.30
1.7.29
1.7.28
1.7.27
1.7.26
1.7.25
1.7.24
1.7.23
1.7.22
1.7.21
1.7.20
1.7.19
1.7.18
1.7.17
1.7.16
1.7.15
1.7.14
1.7.13
1.7.12
1.7.11
1.7.10
1.7.9
1.7.8
1.7.7
1.7.6
1.7.5
1.7.4
1.7.3
1.7.2
1.7.1
1.7.0
1.6.20
1.6.19
1.6.18
1.6.17
1.6.16
1.6.15
1.6.14
1.6.13
1.6.12
1.6.11
1.6.10
1.6.9
1.6.8
1.6.7
1.6.6
1.6.5
1.6.4
1.6.3
1.5.2
1.5.1
1.5.0
1.4.9
1.4.8
1.4.7
1.4.6
1.4.5
1.4.4
1.4.3
1.4.2
1.4.1
1.4.0
1.3.2
1.3.1
1.3.0
1.2.10
1.2.9
1.2.8
1.2.7
1.2.5
1.2.4
1.2.2
1.2.1
1.2.0
1.1.0
1.0.0
A foundation for building Elixir Phoenix apps — SaaS, social networks, ERP systems, marketplaces, and more
Current section
Files
Jump to
Current section
Files
lib/phoenix_kit/dashboard/context_selector.ex
defmodule PhoenixKit.Dashboard.ContextSelector do
@moduledoc """
Configuration and helpers for the dashboard context selector.
The context selector allows users to switch between multiple contexts
(organizations, farms, teams, workspaces, etc.) in the dashboard.
Users with only one context won't see the selector.
## Single Selector Configuration (Legacy)
Configure in your `config/config.exs`:
config :phoenix_kit, :dashboard_context_selector,
loader: {MyApp.Farms, :list_for_user},
display_name: fn farm -> farm.name end,
id_field: :uuid,
label: "Farm",
icon: "hero-building-office",
position: :sidebar,
sub_position: :end,
empty_behavior: :hide,
session_key: "dashboard_context_uuid",
tab_loader: {MyApp.Farms, :get_tabs_for_context}
## Multiple Selectors Configuration
Configure multiple independent or dependent selectors:
config :phoenix_kit, :dashboard_context_selectors, [
%{
key: :organization,
loader: {MyApp.Orgs, :list_for_user},
display_name: fn org -> org.name end,
label: "Organization",
icon: "hero-building-office",
position: :header,
sub_position: :start,
priority: 100
},
%{
key: :project,
depends_on: :organization,
loader: {MyApp.Projects, :list_for_org},
display_name: fn p -> p.name end,
label: "Project",
icon: "hero-folder",
position: :header,
sub_position: :end,
priority: 200,
on_parent_change: :reset
}
]
## Configuration Options
- `:key` - Required for multi-selector. Unique atom identifier (e.g., `:organization`).
Used in session storage and routes.
- `:loader` - Required. A `{Module, :function}` tuple that takes a user ID
and returns a list of context items. Example: `{MyApp.Farms, :list_for_user}`
For dependent selectors, the function receives `(user_uuid, parent_context)`.
- `:display_name` - Required. A function that takes a context item and returns
the display string. Example: `fn farm -> farm.name end`
- `:id_field` - Optional. The field to use as the unique identifier.
Defaults to `:id`. Can be an atom or a function.
- `:label` - Optional. The label shown in the UI (e.g., "Farm", "Organization").
Defaults to `"Context"`.
- `:icon` - Optional. Heroicon name for the selector. Defaults to `"hero-building-office"`.
- `:position` - Optional. Which area to show the selector in.
Options: `:header` (default), `:sidebar`.
- `:sub_position` - Optional. Where within the area to place the selector.
For header: `:start` (left, after logo), `:end` (right, before user menu),
or `{:priority, N}` to sort among other header items.
For sidebar: `:start` (top), `:end` (pinned to very bottom),
or `{:priority, N}` to sort among tabs.
Defaults to `:start`.
- `:priority` - Optional. Integer for ordering within same position/sub_position.
Lower values come first. Defaults to 500.
- `:depends_on` - Optional. Key of parent selector (for dependent selectors).
When set, the loader receives `(user_uuid, parent_context)` instead of just `user_uuid`.
- `:on_parent_change` - Optional. What to do when parent selector changes.
Options: `:reset` (default, select first), `:keep`, `{:redirect, "/path"}`.
- `:empty_behavior` - Optional. What to do when user has no contexts.
Options: `:hide` (default), `:show_empty`, `{:redirect, "/path"}`.
- `:separator` - Optional. Separator shown between logo and selector in header
(only applies to `position: :header, sub_position: :start`).
Defaults to `"/"`. Set to `false` or `nil` to disable. Can be any string
like `"›"`, `"|"`, or `"·"`.
Note: The separator may appear slightly off-center due to internal padding
in the selector dropdown. This is a visual quirk and can be adjusted by
customizing the layout template if precise alignment is required.
- `:session_key` - Optional. The session key for storing the selected context UUID.
Defaults to `"dashboard_context_uuid"` for single selector, or
`"dashboard_context_uuids"` (map) for multiple selectors.
- `:tab_loader` - Optional. A `{Module, :function}` tuple that takes a context
item and returns a list of tab definitions. Enables dynamic tabs that change
based on the selected context. Example: `{MyApp.Farms, :get_tabs_for_context}`
## Usage in LiveViews
The `ContextProvider` on_mount hook automatically sets these assigns:
### Single Selector (Legacy)
- `@dashboard_contexts` - List of all contexts for the user
- `@current_context` - The currently selected context item
- `@show_context_selector` - Boolean, true only if user has 2+ contexts
- `@dashboard_tabs` - (Optional) List of Tab structs when `tab_loader` is configured
### Multiple Selectors
- `@dashboard_contexts_map` - Map of key => list of contexts
- `@current_contexts_map` - Map of key => current context item
- `@show_context_selectors_map` - Map of key => boolean
- `@context_selector_configs` - List of all ContextSelector configs
Access the current context in your LiveView:
def mount(_params, _session, socket) do
context = socket.assigns.current_context
items = MyApp.Items.list_for_context(context.id)
{:ok, assign(socket, items: items)}
end
Or use the helper functions:
context_uuid = PhoenixKit.Dashboard.current_context_uuid(socket)
"""
alias PhoenixKit.Config
defstruct [
:key,
:loader,
:display_name,
:id_field,
:label,
:icon,
:position,
:sub_position,
:priority,
:depends_on,
:on_parent_change,
:empty_behavior,
:session_key,
:tab_loader,
:separator,
enabled: false
]
@type sub_position :: :start | :end | {:priority, integer()}
@type on_parent_change :: :reset | :keep | {:redirect, String.t()}
@type t :: %__MODULE__{
key: atom() | nil,
loader: {module(), atom()} | nil,
display_name: (any() -> String.t()) | nil,
id_field: atom() | (any() -> any()),
label: String.t(),
icon: String.t() | nil,
position: :header | :sidebar,
sub_position: sub_position() | nil,
priority: integer(),
depends_on: atom() | nil,
on_parent_change: on_parent_change(),
empty_behavior: :hide | :show_empty | {:redirect, String.t()},
session_key: String.t(),
tab_loader: {module(), atom()} | nil,
separator: String.t() | false | nil,
enabled: boolean()
}
@default_label "Context"
@default_icon "hero-building-office"
@default_empty_behavior :hide
@default_session_key "dashboard_context_uuid"
@default_multi_session_key "dashboard_context_uuids"
@default_separator "/"
@default_id_field :uuid
@default_priority 500
@default_on_parent_change :reset
@default_key :default
@doc """
Gets the context selector configuration.
Returns a validated `%ContextSelector{}` struct if configured,
or a disabled struct if not configured.
## Examples
iex> PhoenixKit.Dashboard.ContextSelector.get_config()
%ContextSelector{enabled: true, loader: {MyApp.Farms, :list_for_user}, ...}
iex> PhoenixKit.Dashboard.ContextSelector.get_config()
%ContextSelector{enabled: false}
"""
@spec get_config() :: t()
def get_config do
case Config.get(:dashboard_context_selector) do
{:ok, config} when is_map(config) or is_list(config) ->
validate_config(config)
_ ->
%__MODULE__{enabled: false}
end
end
@doc """
Checks if the context selector feature is enabled.
Returns `true` if the feature is configured with a valid loader.
## Examples
iex> PhoenixKit.Dashboard.ContextSelector.enabled?()
true
"""
@spec enabled?() :: boolean()
def enabled? do
get_config().enabled
end
@doc """
Loads contexts for a user using the configured loader.
Returns an empty list if the feature is not enabled or the loader fails.
## Examples
iex> PhoenixKit.Dashboard.ContextSelector.load_contexts(user_uuid)
[%Farm{id: 1, name: "My Farm"}, %Farm{id: 2, name: "Other Farm"}]
"""
@spec load_contexts(any()) :: list()
def load_contexts(user_uuid) do
config = get_config()
if config.enabled do
call_loader(config.loader, user_uuid)
else
[]
end
end
@doc """
Gets the display name for a context item using the configured function.
## Examples
iex> PhoenixKit.Dashboard.ContextSelector.get_display_name(farm)
"My Farm"
"""
@spec get_display_name(any()) :: String.t()
def get_display_name(nil), do: ""
def get_display_name(item) do
config = get_config()
if config.enabled and is_function(config.display_name, 1) do
case config.display_name.(item) do
nil -> ""
result -> result
end
else
to_string(item)
end
rescue
_ -> to_string(item)
end
@doc """
Gets the ID for a context item using the configured id_field.
## Examples
iex> PhoenixKit.Dashboard.ContextSelector.get_id(%{id: 123})
123
"""
@spec get_id(any()) :: any()
def get_id(nil), do: nil
def get_id(item) do
config = get_config()
cond do
is_function(config.id_field, 1) ->
config.id_field.(item)
is_atom(config.id_field) and not is_nil(config.id_field) ->
get_field(item, config.id_field)
true ->
get_field(item, :id)
end
rescue
_ -> nil
end
@doc """
Finds a context by ID from a list of contexts.
Handles both string and integer ID comparison.
## Examples
iex> PhoenixKit.Dashboard.ContextSelector.find_by_id(contexts, "123")
%Farm{id: 123, ...}
"""
@spec find_by_id(list(), any()) :: any() | nil
def find_by_id(contexts, id) when is_list(contexts) do
Enum.find(contexts, fn item ->
item_id = get_id(item)
ids_match?(item_id, id)
end)
end
def find_by_id(_, _), do: nil
@doc """
Gets the session key for storing the context ID.
"""
@spec session_key() :: String.t()
def session_key do
get_config().session_key
end
@doc """
Loads tabs for the given context using the configured tab_loader.
Returns an empty list if no tab_loader is configured or if the loader fails.
## Examples
iex> PhoenixKit.Dashboard.ContextSelector.load_tabs(context)
[%{id: :overview, label: "Overview", ...}, ...]
"""
@spec load_tabs(any()) :: list()
def load_tabs(context) do
config = get_config()
if config.enabled and config.tab_loader do
call_tab_loader(config.tab_loader, context)
else
[]
end
end
defp call_tab_loader({module, function}, context) do
apply(module, function, [context])
rescue
_ -> []
end
# Private functions
defp validate_config(config) when is_list(config) do
validate_config(Map.new(config))
end
defp validate_config(config) when is_map(config) do
loader = get_config_value(config, :loader)
display_name = get_config_value(config, :display_name)
if valid_loader?(loader) and is_function(display_name, 1) do
build_enabled_config(config, loader, display_name)
else
%__MODULE__{enabled: false}
end
end
defp validate_config(_), do: %__MODULE__{enabled: false}
defp build_enabled_config(config, loader, display_name) do
tab_loader = get_config_value(config, :tab_loader)
raw_position = get_config_value(config, :position)
raw_sub_position = get_config_value(config, :sub_position)
key = get_config_value(config, :key)
{position, sub_position} = parse_position_and_sub(raw_position, raw_sub_position)
%__MODULE__{
enabled: true,
key: parse_key(key),
loader: loader,
display_name: display_name,
id_field: get_config_value(config, :id_field, @default_id_field),
label: get_config_value(config, :label, @default_label),
icon: get_config_value(config, :icon, @default_icon),
position: position,
sub_position: sub_position,
priority: get_config_value(config, :priority, @default_priority),
depends_on: parse_key(get_config_value(config, :depends_on)),
on_parent_change: parse_on_parent_change(get_config_value(config, :on_parent_change)),
empty_behavior: config |> get_config_value(:empty_behavior) |> parse_empty_behavior(),
session_key: get_config_value(config, :session_key, @default_session_key),
tab_loader: validate_tab_loader(tab_loader),
separator: parse_separator(get_config_value(config, :separator, @default_separator))
}
end
defp validate_tab_loader({module, function}) when is_atom(module) and is_atom(function) do
{module, function}
end
defp validate_tab_loader(_), do: nil
defp get_config_value(config, key, default \\ nil) do
config[key] || config[to_string(key)] || default
end
defp valid_loader?({module, function}) when is_atom(module) and is_atom(function) do
true
end
defp valid_loader?(_), do: false
defp call_loader({module, function}, user_uuid) do
apply(module, function, [user_uuid])
rescue
_ -> []
end
# Parse position and sub_position
# Returns {position, sub_position} tuple with defaults applied
defp parse_position_and_sub(position, sub_position) do
case normalize_position(position) do
{:header, default_sub} ->
{:header, parse_sub_position(sub_position, default_sub)}
{:sidebar, default_sub} ->
{:sidebar, parse_sub_position(sub_position, default_sub)}
end
end
# Normalize position values to {area, default_sub_position}
defp normalize_position(:header), do: {:header, :start}
defp normalize_position("header"), do: {:header, :start}
defp normalize_position(:sidebar), do: {:sidebar, :start}
defp normalize_position("sidebar"), do: {:sidebar, :start}
defp normalize_position(_), do: {:header, :start}
# Parse sub_position, falling back to default if not specified
defp parse_sub_position(nil, default), do: default
defp parse_sub_position(:start, _default), do: :start
defp parse_sub_position("start", _default), do: :start
defp parse_sub_position(:end, _default), do: :end
defp parse_sub_position("end", _default), do: :end
defp parse_sub_position({:priority, n}, _default) when is_integer(n), do: {:priority, n}
defp parse_sub_position(_, default), do: default
defp parse_empty_behavior(:hide), do: :hide
defp parse_empty_behavior(:show_empty), do: :show_empty
defp parse_empty_behavior({:redirect, path}) when is_binary(path), do: {:redirect, path}
defp parse_empty_behavior("hide"), do: :hide
defp parse_empty_behavior("show_empty"), do: :show_empty
defp parse_empty_behavior(_), do: @default_empty_behavior
# Parse separator - can be a string, false/nil to disable, or default "/"
defp parse_separator(false), do: nil
defp parse_separator(nil), do: nil
defp parse_separator(""), do: nil
defp parse_separator(sep) when is_binary(sep), do: sep
defp parse_separator(_), do: @default_separator
defp get_field(item, field) when is_map(item), do: Map.get(item, field)
defp get_field(item, field) when is_atom(field), do: Map.get(item, field)
defp get_field(_, _), do: nil
defp ids_match?(id1, id2) when is_integer(id1) and is_binary(id2) do
id1 == String.to_integer(id2)
rescue
_ -> false
end
defp ids_match?(id1, id2) when is_binary(id1) and is_integer(id2) do
String.to_integer(id1) == id2
rescue
_ -> false
end
defp ids_match?(id1, id2), do: id1 == id2
# Parse key - convert string to atom, keep atoms as-is.
# Uses to_existing_atom so a misconfigured selector key from runtime/JSON config
# can't grow the atom table unboundedly. Valid keys are always defined as atoms
# in code (e.g. config :phoenix_kit, :dashboard_context_selectors, [%{key: :org, ...}]).
defp parse_key(nil), do: nil
defp parse_key(key) when is_atom(key), do: key
defp parse_key(key) when is_binary(key) do
String.to_existing_atom(key)
rescue
ArgumentError -> nil
end
defp parse_key(_), do: nil
# Parse on_parent_change option
defp parse_on_parent_change(:reset), do: :reset
defp parse_on_parent_change("reset"), do: :reset
defp parse_on_parent_change(:keep), do: :keep
defp parse_on_parent_change("keep"), do: :keep
defp parse_on_parent_change({:redirect, path}) when is_binary(path), do: {:redirect, path}
defp parse_on_parent_change(nil), do: @default_on_parent_change
defp parse_on_parent_change(_), do: @default_on_parent_change
# ============================================================================
# Multi-Selector Support
# ============================================================================
@doc """
Checks if multiple selectors are configured.
Returns `true` if the plural `:dashboard_context_selectors` config is set.
"""
@spec multi_selector_enabled?() :: boolean()
def multi_selector_enabled? do
case Config.get(:dashboard_context_selectors) do
{:ok, [_ | _]} -> true
_ -> false
end
end
@doc """
Gets all context selector configurations.
Returns a list of validated `%ContextSelector{}` structs.
Handles both the legacy single selector config (`:dashboard_context_selector`)
and the new multi-selector config (`:dashboard_context_selectors`).
For legacy single selectors, assigns the default key `:default`.
## Examples
iex> PhoenixKit.Dashboard.ContextSelector.get_all_configs()
[%ContextSelector{key: :organization, ...}, %ContextSelector{key: :project, ...}]
"""
@spec get_all_configs() :: [t()]
def get_all_configs do
case Config.get(:dashboard_context_selectors) do
{:ok, [_ | _] = selectors} ->
selectors
|> Enum.map(&validate_multi_config/1)
|> Enum.filter(& &1.enabled)
|> validate_no_circular_dependencies()
_ ->
# Fallback to legacy single selector
config = get_config()
if config.enabled do
# Ensure legacy config has a key
[%{config | key: config.key || @default_key}]
else
[]
end
end
end
@doc """
Orders selectors by their dependencies using topological sort.
Independent selectors come first, then dependent selectors in order
of their dependency chain. Selectors with the same dependency level
are sorted by priority (lower first).
## Examples
iex> configs = [%{key: :project, depends_on: :org}, %{key: :org}]
iex> PhoenixKit.Dashboard.ContextSelector.order_by_dependencies(configs)
[%{key: :org}, %{key: :project, depends_on: :org}]
"""
@spec order_by_dependencies([t()]) :: [t()]
def order_by_dependencies(configs) when is_list(configs) do
# Build dependency graph
key_to_config = Map.new(configs, fn c -> {c.key, c} end)
# Topological sort using Kahn's algorithm
{sorted, _} = topological_sort(configs, key_to_config)
# Sort by priority within same dependency level
sorted
|> Enum.sort_by(& &1.priority)
end
@doc """
Gets the keys of all selectors that depend on the given key.
Useful for determining which selectors need to be reset when a parent changes.
## Examples
iex> configs = [%{key: :org}, %{key: :project, depends_on: :org}]
iex> PhoenixKit.Dashboard.ContextSelector.get_dependent_keys(configs, :org)
[:project]
"""
@spec get_dependent_keys([t()], atom()) :: [atom()]
def get_dependent_keys(configs, parent_key) when is_list(configs) and is_atom(parent_key) do
configs
|> Enum.filter(fn c -> c.depends_on == parent_key end)
|> Enum.map(& &1.key)
end
@doc """
Loads contexts for a specific selector config.
For independent selectors, calls `loader(user_uuid)`.
For dependent selectors, calls `loader(user_uuid, parent_context)`.
## Parameters
- `config` - The selector configuration
- `user_uuid` - The user UUID to load contexts for
- `parent_context` - The parent context (required for dependent selectors)
"""
@spec load_contexts_for_config(t(), any(), any()) :: list()
def load_contexts_for_config(config, user_uuid, parent_context \\ nil)
def load_contexts_for_config(%__MODULE__{enabled: false}, _user_uuid, _parent_context), do: []
def load_contexts_for_config(%__MODULE__{depends_on: nil} = config, user_uuid, _parent_context) do
# Independent selector - call with just user_uuid
call_loader(config.loader, user_uuid)
end
def load_contexts_for_config(%__MODULE__{depends_on: _} = config, user_uuid, nil) do
# Dependent selector but no parent context - return empty
# This can happen if parent selector has no items
call_loader_with_parent(config.loader, user_uuid, nil)
end
def load_contexts_for_config(%__MODULE__{depends_on: _} = config, user_uuid, parent_context) do
# Dependent selector - call with user_uuid and parent context
call_loader_with_parent(config.loader, user_uuid, parent_context)
end
@doc """
Gets the session key for storing multiple context IDs.
Returns the configured multi-selector session key.
"""
@spec multi_session_key() :: String.t()
def multi_session_key do
@default_multi_session_key
end
@doc """
Gets the display name for a context item using a specific config.
## Examples
iex> PhoenixKit.Dashboard.ContextSelector.get_display_name_for_config(config, item)
"My Organization"
"""
@spec get_display_name_for_config(t(), any()) :: String.t()
def get_display_name_for_config(_config, nil), do: ""
def get_display_name_for_config(%__MODULE__{display_name: display_fn}, item)
when is_function(display_fn, 1) do
case display_fn.(item) do
nil -> ""
result -> result
end
rescue
_ -> to_string(item)
end
def get_display_name_for_config(_config, item), do: to_string(item)
@doc """
Gets the ID for a context item using a specific config.
"""
@spec get_id_for_config(t(), any()) :: any()
def get_id_for_config(_config, nil), do: nil
def get_id_for_config(%__MODULE__{id_field: id_field}, item) do
cond do
is_function(id_field, 1) -> id_field.(item)
is_atom(id_field) and not is_nil(id_field) -> get_field(item, id_field)
true -> get_field(item, :id)
end
rescue
_ -> nil
end
# Private multi-selector helpers
defp validate_multi_config(config) when is_map(config) do
# Multi-selector requires a key
key = get_config_value(config, :key)
if key do
validated = validate_config(config)
# Ensure key is set even if validate_config didn't set it
%{validated | key: parse_key(key) || validated.key}
else
%__MODULE__{enabled: false}
end
end
defp validate_multi_config(config) when is_list(config) do
validate_multi_config(Map.new(config))
end
defp validate_multi_config(_), do: %__MODULE__{enabled: false}
defp validate_no_circular_dependencies(configs) do
keys = MapSet.new(configs, & &1.key)
has_circular =
Enum.any?(configs, fn config ->
config.depends_on &&
MapSet.member?(keys, config.depends_on) &&
check_circular_dependency(config.key, config.depends_on, configs, MapSet.new())
end)
if has_circular do
# Return only independent selectors on circular dependency
Enum.filter(configs, fn c -> is_nil(c.depends_on) end)
else
configs
end
end
@spec check_circular_dependency(atom(), atom(), [t()], MapSet.t(atom())) :: boolean()
defp check_circular_dependency(original_key, current_key, configs, visited) do
if MapSet.member?(visited, current_key) do
# Already visited - this is a cycle
true
else
visited = MapSet.put(visited, current_key)
config = Enum.find(configs, fn c -> c.key == current_key end)
check_config_for_cycle(original_key, config, configs, visited)
end
end
@spec check_config_for_cycle(atom(), t() | nil, [t()], MapSet.t(atom())) :: boolean()
defp check_config_for_cycle(_original_key, nil, _configs, _visited), do: false
defp check_config_for_cycle(original_key, config, _configs, _visited)
when config.depends_on == original_key,
do: true
defp check_config_for_cycle(_original_key, config, _configs, _visited)
when is_nil(config.depends_on),
do: false
defp check_config_for_cycle(original_key, config, configs, visited) do
check_circular_dependency(original_key, config.depends_on, configs, visited)
end
defp topological_sort(configs, key_to_config) do
# Start with configs that have no dependencies
{independent, dependent} =
Enum.split_with(configs, fn c -> is_nil(c.depends_on) end)
# Process dependent configs in order
process_dependent(
independent,
dependent,
key_to_config,
MapSet.new(Enum.map(independent, & &1.key))
)
end
defp process_dependent(sorted, [], _key_to_config, _processed_keys), do: {sorted, []}
defp process_dependent(sorted, remaining, key_to_config, processed_keys) do
# Find configs whose dependencies have been processed
{ready, not_ready} =
Enum.split_with(remaining, fn c ->
MapSet.member?(processed_keys, c.depends_on)
end)
if ready == [] do
# No progress - remaining have unresolved dependencies
{sorted, remaining}
else
new_sorted = sorted ++ ready
new_processed = MapSet.union(processed_keys, MapSet.new(ready, & &1.key))
process_dependent(new_sorted, not_ready, key_to_config, new_processed)
end
end
defp call_loader_with_parent({module, function}, user_uuid, parent_context) do
apply(module, function, [user_uuid, parent_context])
rescue
_ -> []
end
end