Packages
phoenix_kit
1.7.21
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/modules/sitemap/route_resolver.ex
defmodule PhoenixKit.Modules.Sitemap.RouteResolver do
@moduledoc """
Resolves actual routes from parent application router.
Uses router introspection to automatically detect URL patterns,
falling back to Settings configuration when introspection fails.
## Resolution Priority
1. Router Introspection - automatic detection from parent app router
2. Settings override - manual configuration in PhoenixKit Settings
3. Hardcoded fallback - default values
## Usage
# Find path for specific plug module
RouteResolver.find_route(PhoenixKitWeb.Users.Registration)
# => "/users/register"
# Find content route by type
RouteResolver.find_content_route(:pages)
# => "/pages/:slug"
RouteResolver.find_content_route(:entity, "product")
# => "/products/:slug"
"""
require Logger
@doc """
Gets router module with automatic discovery.
Resolution order:
1. `config :phoenix_kit, router: MyAppWeb.Router`
2. Via endpoint from `config :phoenix_kit, endpoint: MyAppWeb.Endpoint`
3. Auto-discover from OTP applications (finds *Web.Router modules)
"""
def get_router do
# 1. Explicit router config
case PhoenixKit.Config.get(:router) do
{:ok, router} when not is_nil(router) ->
if valid_router?(router), do: router, else: try_endpoint_router()
_ ->
try_endpoint_router()
end
end
defp try_endpoint_router do
# 2. Get router through endpoint
case PhoenixKit.Config.get(:endpoint) do
{:ok, endpoint} when not is_nil(endpoint) ->
router = get_router_from_endpoint(endpoint)
if router, do: router, else: try_auto_discover()
_ ->
try_auto_discover()
end
end
defp get_router_from_endpoint(endpoint) do
if Code.ensure_loaded?(endpoint) do
try do
router = endpoint.config(:router)
if valid_router?(router), do: router, else: nil
rescue
_ -> nil
end
else
nil
end
end
# Auto-discover router from loaded OTP applications
defp try_auto_discover do
# Get all loaded applications except known system ones
excluded_apps = ~w(
elixir stdlib kernel compiler phoenix phoenix_live_view phoenix_html
plug ecto ecto_sql postgrex jason swoosh oban hammer bcrypt_elixir
argon2_elixir telemetry logger gettext phoenix_pubsub castore
mint finch req nimble_options phoenix_ecto floki html_entities
ex_doc makeup makeup_elixir makeup_erlang earmark_parser mdex
timex tzdata combine file_system esbuild tailwind heroicons
phoenix_kit ueberauth ueberauth_google ueberauth_github
ueberauth_apple ueberauth_facebook assent jose igniter
owl sourceror spitfire rewrite glob_ex infer inflex
phoenix_live_dashboard observer_cli
)a
:application.loaded_applications()
|> Enum.map(fn {app, _, _} -> app end)
|> Enum.reject(&(&1 in excluded_apps))
|> Enum.find_value(&find_router_in_app/1)
end
defp find_router_in_app(app) do
# Try common router module naming patterns
app_name = app |> to_string() |> Macro.camelize()
patterns = [
"#{app_name}Web.Router",
"#{app_name}.Router",
"#{app_name}Web.Web.Router"
]
Enum.find_value(patterns, fn pattern ->
module = String.to_atom("Elixir.#{pattern}")
if valid_router?(module) do
Logger.debug("RouteResolver: Auto-discovered router #{inspect(module)}")
module
else
nil
end
end)
end
defp valid_router?(nil), do: false
defp valid_router?(router) do
Code.ensure_loaded?(router) and
function_exported?(router, :__routes__, 0)
end
@doc """
Returns all routes from the parent router.
Returns empty list if router is not available.
"""
@spec get_routes() :: [map()]
def get_routes do
case get_router() do
nil ->
[]
router ->
try do
router.__routes__()
rescue
error ->
Logger.debug("RouteResolver: Failed to get routes: #{inspect(error)}")
[]
end
end
end
@doc """
Finds path for a specific plug module.
## Options
- `:verb` - HTTP verb to match (default: `:get`)
## Examples
find_route(PhoenixKitWeb.Users.Registration)
# => "/users/register"
find_route(MyApp.SomeController, verb: :post)
# => "/some/path"
"""
@spec find_route(module(), keyword()) :: String.t() | nil
def find_route(plug_module, opts \\ []) do
verb = Keyword.get(opts, :verb, :get)
get_routes()
|> Enum.find(fn route ->
route.plug == plug_module and route.verb == verb
end)
|> case do
nil -> nil
route -> route.path
end
end
@doc """
Finds route pattern by content type.
## Types
- `:pages` - Finds routes that look like page routes (contain :slug and plug name contains "page")
- `:posts` - Finds routes that look like post routes (contain :slug and plug name contains "post")
- `:entity` - Finds routes matching entity name (singular or plural form)
## Examples
find_content_route(:pages)
# => "/pages/:slug"
find_content_route(:posts)
# => "/posts/:slug"
find_content_route(:entity, "product")
# => "/products/:slug"
find_content_route(:entity, "page")
# => "/pages/:slug"
"""
@spec find_content_route(atom(), String.t() | nil) :: String.t() | nil
def find_content_route(type, name \\ nil)
def find_content_route(:pages, _name) do
get_routes()
|> Enum.filter(fn route ->
route.verb == :get and
(String.contains?(route.path, ":slug") or
String.contains?(route.path, "*path"))
end)
|> Enum.find(fn route ->
plug_name = to_string(route.plug) |> String.downcase()
String.contains?(plug_name, "page") or String.contains?(plug_name, "content")
end)
|> extract_path()
end
def find_content_route(:posts, _name) do
get_routes()
|> Enum.filter(fn route ->
route.verb == :get and
(String.contains?(route.path, ":slug") or String.contains?(route.path, ":id"))
end)
|> Enum.find(fn route ->
path_lower = String.downcase(route.path)
plug_name = to_string(route.plug) |> String.downcase()
# Match routes with /posts/ in path or plug name containing "post"
String.contains?(path_lower, "/posts/") or
String.starts_with?(path_lower, "/posts/") or
(String.contains?(plug_name, "post") and not String.contains?(plug_name, "page"))
end)
|> extract_path()
end
def find_content_route(:entity, entity_name) when is_binary(entity_name) do
entity_lower = String.downcase(entity_name)
routes =
get_routes()
|> Enum.filter(fn route ->
route.verb == :get and
(String.contains?(route.path, ":slug") or String.contains?(route.path, ":id"))
end)
# First try exact entity name match
exact_match =
Enum.find(routes, fn route ->
path_lower = String.downcase(route.path)
# Match both singular and plural forms in path
String.contains?(path_lower, "/#{entity_lower}/") or
String.contains?(path_lower, "/#{entity_lower}s/") or
String.starts_with?(path_lower, "/#{entity_lower}/") or
String.starts_with?(path_lower, "/#{entity_lower}s/")
end)
if exact_match do
extract_path(exact_match)
else
# Try catch-all pattern like /:entity_name/:slug or /:name/:slug
find_catchall_entity_route(routes, entity_name)
end
end
def find_content_route(_, _), do: nil
@doc """
Finds index route for content type (list page without :slug).
## Examples
find_index_route(:posts)
# => "/posts"
find_index_route(:entity, "page")
# => "/page" or "/pages"
find_index_route(:entity, "product")
# => "/products"
"""
@spec find_index_route(atom(), String.t() | nil) :: String.t() | nil
def find_index_route(type, name \\ nil)
def find_index_route(:posts, _name) do
get_routes()
|> Enum.filter(fn route ->
route.verb == :get and
not String.contains?(route.path, ":") and
not String.contains?(route.path, "*")
end)
|> Enum.find(fn route ->
path_lower = String.downcase(route.path)
plug_name = to_string(route.plug) |> String.downcase()
# Match /posts path or plug name containing "post"
path_lower == "/posts" or
String.ends_with?(path_lower, "/posts") or
(String.contains?(plug_name, "post") and not String.contains?(plug_name, "page") and
not String.contains?(path_lower, ":"))
end)
|> extract_path()
end
def find_index_route(:entity, entity_name) when is_binary(entity_name) do
entity_lower = String.downcase(entity_name)
# First try static routes (without params)
static_routes =
get_routes()
|> Enum.filter(fn route ->
route.verb == :get and
not String.contains?(route.path, ":") and
not String.contains?(route.path, "*")
end)
exact_match =
Enum.find(static_routes, fn route ->
path_lower = String.downcase(route.path)
# Match exact entity path or plural form
path_lower == "/#{entity_lower}" or
path_lower == "/#{entity_lower}s" or
String.ends_with?(path_lower, "/#{entity_lower}") or
String.ends_with?(path_lower, "/#{entity_lower}s")
end)
if exact_match do
extract_path(exact_match)
else
# Try catch-all pattern like /:entity_name
param_routes =
get_routes()
|> Enum.filter(fn route ->
route.verb == :get and
String.contains?(route.path, ":") and
not String.contains?(route.path, "*")
end)
find_catchall_index_route(param_routes, entity_name)
end
end
def find_index_route(_, _), do: nil
@doc """
Extracts URL prefix from a route pattern.
## Examples
extract_prefix("/pages/:slug")
# => "/pages"
extract_prefix("/content/*path")
# => "/content"
extract_prefix("/blog/posts/:id")
# => "/blog/posts"
"""
@spec extract_prefix(String.t() | nil) :: String.t() | nil
def extract_prefix(nil), do: nil
def extract_prefix(pattern) when is_binary(pattern) do
pattern
|> String.split("/:")
|> List.first()
|> String.split("/*")
|> List.first()
|> case do
"" -> "/"
prefix -> prefix
end
end
# Private helpers
# Find catch-all entity routes like /:entity_name/:slug
# Returns the pattern with :entity_name replaced by actual entity name
defp find_catchall_entity_route(routes, entity_name) do
catchall_patterns = [
~r{^/:entity_name/:slug$},
~r{^/:entity/:slug$},
~r{^/:name/:slug$},
~r{^/:type/:slug$},
~r{^/:[a-z_]+/:slug$}
]
Enum.find_value(routes, fn route ->
if Enum.any?(catchall_patterns, &Regex.match?(&1, route.path)) do
# Replace the first param with entity name
route.path
|> String.replace(~r{^/:[a-z_]+/}, "/#{entity_name}/")
else
nil
end
end)
end
# Find catch-all index routes like /:entity_name
defp find_catchall_index_route(routes, entity_name) do
catchall_patterns = [
~r{^/:entity_name$},
~r{^/:entity$},
~r{^/:name$},
~r{^/:type$},
~r{^/:[a-z_]+$}
]
Enum.find_value(routes, fn route ->
if Enum.any?(catchall_patterns, &Regex.match?(&1, route.path)) do
# Replace with entity name
"/#{entity_name}"
else
nil
end
end)
end
defp extract_path(nil), do: nil
defp extract_path(route), do: route.path
@doc """
Checks if a route requires authentication based on its on_mount hooks.
Returns true if the route uses authentication-requiring on_mount hooks:
- `:phoenix_kit_ensure_authenticated_scope`
- `:phoenix_kit_ensure_admin`
## Examples
route_requires_auth?(%{metadata: %{...}})
# => true/false
# Check by path pattern
route_requires_auth?("/posts")
# => true/false
"""
@spec route_requires_auth?(map() | String.t()) :: boolean()
def route_requires_auth?(route) when is_map(route) do
on_mount_hooks = extract_on_mount_hooks(route)
Enum.any?(on_mount_hooks, fn hook ->
hook in [
:phoenix_kit_ensure_authenticated_scope,
:phoenix_kit_ensure_admin,
:ensure_authenticated,
:require_authenticated_user
]
end)
end
def route_requires_auth?(path) when is_binary(path) do
get_routes()
|> Enum.find(fn route ->
route.verb == :get and routes_match?(route.path, path)
end)
|> case do
nil -> false
route -> route_requires_auth?(route)
end
end
@doc """
Checks if a content route (posts, entities, etc.) requires authentication.
## Examples
content_route_requires_auth?(:posts)
# => false
content_route_requires_auth?(:entity, "product")
# => false
"""
@spec content_route_requires_auth?(atom(), String.t() | nil) :: boolean()
def content_route_requires_auth?(type, name \\ nil)
def content_route_requires_auth?(:posts, _name) do
get_routes()
|> Enum.filter(fn route ->
route.verb == :get and
(String.contains?(route.path, ":slug") or String.contains?(route.path, ":id"))
end)
|> Enum.find(fn route ->
path_lower = String.downcase(route.path)
plug_name = to_string(route.plug) |> String.downcase()
String.contains?(path_lower, "/posts/") or
String.starts_with?(path_lower, "/posts/") or
(String.contains?(plug_name, "post") and not String.contains?(plug_name, "page"))
end)
|> case do
nil -> false
route -> route_requires_auth?(route)
end
end
def content_route_requires_auth?(:entity, entity_name) when is_binary(entity_name) do
case find_content_route(:entity, entity_name) do
nil ->
false
path ->
route_requires_auth?(path)
end
end
def content_route_requires_auth?(_, _), do: false
# Extract on_mount hooks from route metadata
defp extract_on_mount_hooks(route) do
case get_in(route.metadata, [:phoenix_live_view]) do
{_, _, _, %{extra: %{on_mount: on_mount}}} ->
Enum.map(on_mount, fn
%{id: {_mod, id}} -> id
{_mod, id} -> id
_ -> nil
end)
|> Enum.reject(&is_nil/1)
_ ->
[]
end
rescue
_ -> []
end
# Check if route patterns match (handles :params and *wildcards)
defp routes_match?(pattern, path) do
pattern_parts = String.split(pattern, "/")
path_parts = String.split(path, "/")
if length(pattern_parts) != length(path_parts) do
false
else
Enum.zip(pattern_parts, path_parts)
|> Enum.all?(fn
{":" <> _, _} -> true
{"*" <> _, _} -> true
{same, same} -> true
_ -> false
end)
end
end
end