mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-13 03:06:15 +00:00
401 lines
9.8 KiB
Elixir
401 lines
9.8 KiB
Elixir
defmodule WandererAppWeb.ApiRouter.Introspection do
|
|
@moduledoc """
|
|
Route introspection and documentation generation for the API.
|
|
|
|
This module provides utilities for analyzing routes, generating
|
|
documentation, and creating OpenAPI specifications.
|
|
"""
|
|
|
|
alias WandererAppWeb.{ApiRoutes, ApiRouter.RouteSpec}
|
|
|
|
@doc """
|
|
List all routes for a specific version or all versions.
|
|
"""
|
|
def list_routes(version \\ nil) do
|
|
case version do
|
|
nil -> ApiRoutes.table()
|
|
v -> %{v => ApiRoutes.routes_for_version(v)}
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Find route information by version, method, and path.
|
|
"""
|
|
def route_info(version, method, path) do
|
|
routes = ApiRoutes.routes_for_version(version)
|
|
method_atom = method |> String.downcase() |> String.to_atom()
|
|
normalized_path = normalize_path(path)
|
|
|
|
Enum.find(routes, fn route_spec ->
|
|
route_spec.verb == method_atom and
|
|
path_matches?(normalized_path, route_spec.path)
|
|
end)
|
|
end
|
|
|
|
@doc """
|
|
Get all routes that match a specific controller.
|
|
"""
|
|
def routes_for_controller(controller, version \\ nil) do
|
|
routes =
|
|
case version do
|
|
nil ->
|
|
ApiRoutes.table()
|
|
|> Enum.flat_map(fn {_v, routes} -> routes end)
|
|
|
|
v ->
|
|
ApiRoutes.routes_for_version(v)
|
|
end
|
|
|
|
Enum.filter(routes, fn route_spec ->
|
|
route_spec.controller == controller
|
|
end)
|
|
end
|
|
|
|
@doc """
|
|
Get all routes that support a specific feature.
|
|
"""
|
|
def routes_with_feature(feature, version \\ nil) do
|
|
routes =
|
|
case version do
|
|
nil ->
|
|
ApiRoutes.table()
|
|
|> Enum.flat_map(fn {_v, routes} -> routes end)
|
|
|
|
v ->
|
|
ApiRoutes.routes_for_version(v)
|
|
end
|
|
|
|
Enum.filter(routes, fn route_spec ->
|
|
feature in route_spec.features
|
|
end)
|
|
end
|
|
|
|
@doc """
|
|
Generate OpenAPI 3.0 specification for a version.
|
|
"""
|
|
def generate_openapi_spec(version) do
|
|
routes = ApiRoutes.routes_for_version(version)
|
|
|
|
%{
|
|
openapi: "3.0.0",
|
|
info: %{
|
|
title: "Wanderer API",
|
|
version: version,
|
|
description: "EVE Online mapping tool API",
|
|
contact: %{
|
|
name: "Wanderer Support",
|
|
url: "https://docs.wanderer.com"
|
|
}
|
|
},
|
|
servers: [
|
|
%{
|
|
url: "/api/v#{version}",
|
|
description: "API v#{version} endpoint"
|
|
}
|
|
],
|
|
paths: generate_paths(routes),
|
|
components: %{
|
|
securitySchemes: %{
|
|
bearerAuth: %{
|
|
type: "http",
|
|
scheme: "bearer",
|
|
bearerFormat: "JWT"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
end
|
|
|
|
@doc """
|
|
Generate a simple route summary for documentation.
|
|
"""
|
|
def generate_route_summary(version \\ "1") do
|
|
routes = ApiRoutes.routes_for_version(version)
|
|
|
|
Enum.group_by(routes, fn route_spec ->
|
|
# Group by controller for better organization
|
|
route_spec.controller
|
|
|> Module.split()
|
|
|> List.last()
|
|
|> String.replace("Controller", "")
|
|
end)
|
|
|> Enum.map(fn {controller_name, controller_routes} ->
|
|
%{
|
|
controller: controller_name,
|
|
routes: Enum.map(controller_routes, &route_to_summary/1)
|
|
}
|
|
end)
|
|
end
|
|
|
|
@doc """
|
|
Validate route definitions and return any issues.
|
|
"""
|
|
def validate_routes(version \\ nil) do
|
|
routes =
|
|
case version do
|
|
nil ->
|
|
ApiRoutes.table()
|
|
|> Enum.flat_map(fn {v, routes} ->
|
|
Enum.map(routes, fn route -> {v, route} end)
|
|
end)
|
|
|
|
v ->
|
|
ApiRoutes.routes_for_version(v)
|
|
|> Enum.map(fn route -> {v, route} end)
|
|
end
|
|
|
|
Enum.reduce(routes, [], fn {v, route_spec}, errors ->
|
|
case RouteSpec.validate(route_spec) do
|
|
{:ok, _} -> errors
|
|
{:error, error} -> [{v, route_spec, error} | errors]
|
|
end
|
|
end)
|
|
end
|
|
|
|
@doc """
|
|
Find duplicate routes (same method and path).
|
|
"""
|
|
def find_duplicate_routes(version \\ nil) do
|
|
routes =
|
|
case version do
|
|
nil ->
|
|
ApiRoutes.table()
|
|
|> Enum.flat_map(fn {v, routes} ->
|
|
Enum.map(routes, fn route -> {v, route} end)
|
|
end)
|
|
|
|
v ->
|
|
ApiRoutes.routes_for_version(v)
|
|
|> Enum.map(fn route -> {v, route} end)
|
|
end
|
|
|
|
routes
|
|
|> Enum.group_by(fn {_v, route_spec} ->
|
|
{route_spec.verb, normalize_path_for_comparison(route_spec.path)}
|
|
end)
|
|
|> Enum.filter(fn {_key, group} -> length(group) > 1 end)
|
|
|> Enum.map(fn {key, duplicates} ->
|
|
%{
|
|
route_signature: key,
|
|
duplicates: duplicates
|
|
}
|
|
end)
|
|
end
|
|
|
|
# Private helper functions
|
|
|
|
defp normalize_path(path) when is_list(path), do: path
|
|
|
|
defp normalize_path(path) when is_binary(path) do
|
|
path
|
|
|> String.split("/")
|
|
|> Enum.reject(&(&1 == ""))
|
|
end
|
|
|
|
defp normalize_path_for_comparison(path) do
|
|
# Replace parameter atoms with a standard placeholder for comparison
|
|
Enum.map(path, fn
|
|
segment when is_atom(segment) -> ":param"
|
|
segment -> segment
|
|
end)
|
|
end
|
|
|
|
defp path_matches?(request_path, route_path) when length(request_path) != length(route_path) do
|
|
false
|
|
end
|
|
|
|
defp path_matches?([], []), do: true
|
|
|
|
defp path_matches?([req_segment | req_rest], [route_segment | route_rest])
|
|
when is_binary(route_segment) do
|
|
req_segment == route_segment and path_matches?(req_rest, route_rest)
|
|
end
|
|
|
|
defp path_matches?([_req_segment | req_rest], [route_segment | route_rest])
|
|
when is_atom(route_segment) do
|
|
# Route segment is a parameter (atom), so it matches any request segment
|
|
path_matches?(req_rest, route_rest)
|
|
end
|
|
|
|
defp path_matches?(_, _), do: false
|
|
|
|
defp generate_paths(routes) do
|
|
routes
|
|
|> Enum.group_by(&openapi_path_key/1)
|
|
|> Enum.reduce(%{}, fn {path_key, path_routes}, acc ->
|
|
operations =
|
|
Enum.reduce(path_routes, %{}, fn route_spec, ops ->
|
|
method_key = Atom.to_string(route_spec.verb)
|
|
operation = generate_operation(route_spec)
|
|
Map.put(ops, method_key, operation)
|
|
end)
|
|
|
|
Map.put(acc, path_key, operations)
|
|
end)
|
|
end
|
|
|
|
defp openapi_path_key(route_spec) do
|
|
"/" <>
|
|
Enum.join(
|
|
Enum.map(route_spec.path, fn
|
|
segment when is_atom(segment) -> "{#{segment}}"
|
|
segment -> segment
|
|
end),
|
|
"/"
|
|
)
|
|
end
|
|
|
|
defp generate_operation(route_spec) do
|
|
metadata = route_spec.metadata
|
|
|
|
%{
|
|
summary: Map.get(metadata, :description, ""),
|
|
operationId: "#{route_spec.controller}_#{route_spec.action}",
|
|
tags: [extract_tag_from_controller(route_spec.controller)],
|
|
parameters: generate_parameters(route_spec),
|
|
responses: generate_responses(route_spec),
|
|
security: if(Map.get(metadata, :auth_required, false), do: [%{bearerAuth: []}], else: [])
|
|
}
|
|
end
|
|
|
|
defp extract_tag_from_controller(controller) do
|
|
controller
|
|
|> Module.split()
|
|
|> List.last()
|
|
|> String.replace(~r/(API)?Controller$/, "")
|
|
end
|
|
|
|
defp generate_parameters(route_spec) do
|
|
# Extract path parameters
|
|
path_params =
|
|
route_spec.path
|
|
|> Enum.filter(&is_atom/1)
|
|
|> Enum.map(fn param ->
|
|
%{
|
|
name: Atom.to_string(param),
|
|
in: "path",
|
|
required: true,
|
|
schema: %{type: "string"}
|
|
}
|
|
end)
|
|
|
|
# Add query parameters based on features
|
|
query_params =
|
|
route_spec.features
|
|
|> Enum.flat_map(&feature_to_parameters/1)
|
|
|
|
path_params ++ query_params
|
|
end
|
|
|
|
defp feature_to_parameters("filtering") do
|
|
[
|
|
%{
|
|
name: "filter",
|
|
in: "query",
|
|
required: false,
|
|
schema: %{type: "object"},
|
|
description: "Filter parameters"
|
|
}
|
|
]
|
|
end
|
|
|
|
defp feature_to_parameters("sorting") do
|
|
[
|
|
%{
|
|
name: "sort",
|
|
in: "query",
|
|
required: false,
|
|
schema: %{type: "string"},
|
|
description: "Sort fields (comma-separated)"
|
|
}
|
|
]
|
|
end
|
|
|
|
defp feature_to_parameters("pagination") do
|
|
[
|
|
%{
|
|
name: "page[number]",
|
|
in: "query",
|
|
required: false,
|
|
schema: %{type: "integer", minimum: 1},
|
|
description: "Page number"
|
|
},
|
|
%{
|
|
name: "page[size]",
|
|
in: "query",
|
|
required: false,
|
|
schema: %{type: "integer", minimum: 1, maximum: 100},
|
|
description: "Page size"
|
|
}
|
|
]
|
|
end
|
|
|
|
defp feature_to_parameters("includes") do
|
|
[
|
|
%{
|
|
name: "include",
|
|
in: "query",
|
|
required: false,
|
|
schema: %{type: "string"},
|
|
description: "Related resources to include (comma-separated)"
|
|
}
|
|
]
|
|
end
|
|
|
|
defp feature_to_parameters("sparse_fieldsets") do
|
|
[
|
|
%{
|
|
name: "fields",
|
|
in: "query",
|
|
required: false,
|
|
schema: %{type: "object"},
|
|
description: "Sparse fieldsets"
|
|
}
|
|
]
|
|
end
|
|
|
|
defp feature_to_parameters(_), do: []
|
|
|
|
defp generate_responses(route_spec) do
|
|
metadata = route_spec.metadata
|
|
success_status = Map.get(metadata, :success_status, 200)
|
|
content_type = Map.get(metadata, :content_type, "application/vnd.api+json")
|
|
|
|
responses = %{
|
|
to_string(success_status) => %{
|
|
description: "Success",
|
|
content: %{
|
|
content_type => %{
|
|
schema: %{type: "object"}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
# Add error responses
|
|
if Map.get(metadata, :auth_required, false) do
|
|
Map.put(responses, "401", %{
|
|
description: "Unauthorized",
|
|
content: %{
|
|
"application/json" => %{
|
|
schema: %{type: "object"}
|
|
}
|
|
}
|
|
})
|
|
else
|
|
responses
|
|
end
|
|
end
|
|
|
|
defp route_to_summary(route_spec) do
|
|
%{
|
|
method: String.upcase(Atom.to_string(route_spec.verb)),
|
|
path: "/" <> Enum.join(route_spec.path, "/"),
|
|
action: route_spec.action,
|
|
features: route_spec.features,
|
|
auth_required: get_in(route_spec.metadata, [:auth_required]),
|
|
description: get_in(route_spec.metadata, [:description])
|
|
}
|
|
end
|
|
end
|