mirror of
https://github.com/wanderer-industries/wanderer
synced 2025-12-02 14:02:37 +00:00
311 lines
10 KiB
Elixir
311 lines
10 KiB
Elixir
defmodule WandererAppWeb.Helpers.APIUtils do
|
|
@moduledoc """
|
|
Unified helper module for API operations:
|
|
- Parameter parsing and validation
|
|
- Map ID resolution
|
|
- Standardized responses
|
|
- JSON serialization
|
|
"""
|
|
|
|
# Explicit imports to avoid unnecessary dependencies
|
|
import Plug.Conn, only: [put_status: 2]
|
|
import Phoenix.Controller, only: [json: 2]
|
|
|
|
alias WandererApp.Api.Map, as: MapApi
|
|
alias WandererApp.Api.MapSolarSystem
|
|
require Logger
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Map ID Resolution
|
|
# -----------------------------------------------------------------------------
|
|
|
|
@spec fetch_map_id(map()) :: {:ok, String.t()} | {:error, String.t()}
|
|
def fetch_map_id(%{"map_id" => id}) when is_binary(id) do
|
|
case Ecto.UUID.cast(id) do
|
|
{:ok, _} -> {:ok, id}
|
|
:error -> {:error, "Invalid UUID format for map_id: #{id}"}
|
|
end
|
|
end
|
|
|
|
def fetch_map_id(%{"slug" => slug}) when is_binary(slug) do
|
|
case MapApi.get_map_by_slug(slug) do
|
|
{:ok, %{id: id}} -> {:ok, id}
|
|
_ -> {:error, "No map found for slug=#{slug}"}
|
|
end
|
|
end
|
|
|
|
def fetch_map_id(_), do: {:error, "Must provide either ?map_id=UUID or ?slug=SLUG"}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Parameter Validators and Parsers
|
|
# -----------------------------------------------------------------------------
|
|
|
|
@spec require_param(map(), String.t()) :: {:ok, any()} | {:error, String.t()}
|
|
def require_param(params, key) do
|
|
case Map.fetch(params, key) do
|
|
{:ok, val} when is_binary(val) ->
|
|
trimmed = String.trim(val)
|
|
if trimmed == "" do
|
|
{:error, "Param #{key} cannot be empty"}
|
|
else
|
|
{:ok, trimmed}
|
|
end
|
|
|
|
{:ok, val} ->
|
|
{:ok, val}
|
|
|
|
:error ->
|
|
{:error, "Missing required param: #{key}"}
|
|
end
|
|
end
|
|
|
|
@spec parse_int(binary() | integer()) :: {:ok, integer()} | {:error, String.t()}
|
|
def parse_int(str) when is_binary(str) do
|
|
Logger.debug("Parsing integer from: #{inspect(str)}")
|
|
|
|
case Integer.parse(str) do
|
|
{num, ""} -> {:ok, num}
|
|
_ -> {:error, "Invalid integer format: #{str}"}
|
|
end
|
|
end
|
|
|
|
def parse_int(num) when is_integer(num), do: {:ok, num}
|
|
def parse_int(other), do: {:error, "Expected integer or string, got: #{inspect(other)}"}
|
|
|
|
@spec parse_int!(binary() | integer()) :: integer()
|
|
def parse_int!(str) do
|
|
case parse_int(str) do
|
|
{:ok, num} -> num
|
|
{:error, msg} -> raise ArgumentError, msg
|
|
end
|
|
end
|
|
|
|
@spec validate_uuid(any()) :: {:ok, String.t()} | {:error, String.t()}
|
|
def validate_uuid(id) when is_binary(id) do
|
|
case Ecto.UUID.cast(id) do
|
|
{:ok, uuid} -> {:ok, uuid}
|
|
:error -> {:error, "Invalid UUID format: #{id}"}
|
|
end
|
|
end
|
|
|
|
def validate_uuid(_), do: {:error, "ID must be a UUID string"}
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Parameter Extraction
|
|
# -----------------------------------------------------------------------------
|
|
|
|
@doc """
|
|
Extract and validate parameters for upserting a system.
|
|
Returns {:ok, attrs} or {:error, error_message}.
|
|
"""
|
|
@spec extract_upsert_params(map()) :: {:ok, map()} | {:error, String.t()}
|
|
def extract_upsert_params(params) when is_map(params) do
|
|
required = ["solar_system_id"]
|
|
optional = [
|
|
"solar_system_name", "position_x", "position_y", "coordinates",
|
|
"status", "visible", "description", "tag",
|
|
"locked", "temporary_name", "labels"
|
|
]
|
|
|
|
case Map.fetch(params, "solar_system_id") do
|
|
:error -> {:error, "Missing solar_system_id in request body"}
|
|
{:ok, _} ->
|
|
params
|
|
|> Map.take(required ++ optional)
|
|
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
|
|> Enum.into(%{})
|
|
|> then(&{:ok, &1})
|
|
end
|
|
end
|
|
|
|
@doc """
|
|
Extract and validate parameters for updating a system.
|
|
Returns {:ok, attrs} or {:error, error_message}.
|
|
"""
|
|
@spec extract_update_params(map()) :: {:ok, map()} | {:error, String.t()}
|
|
def extract_update_params(params) when is_map(params) do
|
|
allowed = [
|
|
"solar_system_name", "position_x", "position_y", "coordinates",
|
|
"status", "visible", "description", "tag",
|
|
"locked", "temporary_name", "labels"
|
|
]
|
|
|
|
attrs =
|
|
params
|
|
|> Map.take(allowed)
|
|
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|
|
|> Enum.into(%{})
|
|
|
|
{:ok, attrs}
|
|
end
|
|
|
|
@spec normalize_connection_params(map()) :: {:ok, map()} | {:error, String.t()}
|
|
def normalize_connection_params(params) do
|
|
# Convert all keys to strings for consistent access
|
|
string_params = for {k, v} <- params, into: %{} do
|
|
{to_string(k), v}
|
|
end
|
|
|
|
# Define parameter mappings for normalization
|
|
aliases = %{
|
|
"source" => "solar_system_source",
|
|
"source_id" => "solar_system_source",
|
|
"target" => "solar_system_target",
|
|
"target_id" => "solar_system_target"
|
|
}
|
|
|
|
# Normalize parameters using aliases
|
|
normalized_params = Enum.reduce(aliases, string_params, fn {alias_key, std_key}, acc ->
|
|
if Map.has_key?(acc, alias_key) && !Map.has_key?(acc, std_key) do
|
|
Map.put(acc, std_key, acc[alias_key])
|
|
else
|
|
acc
|
|
end
|
|
end)
|
|
|
|
# Handle required parameters
|
|
with {:ok, src} <- parse_to_int(normalized_params["solar_system_source"], "solar_system_source"),
|
|
{:ok, tgt} <- parse_to_int(normalized_params["solar_system_target"], "solar_system_target") do
|
|
|
|
# Handle optional parameters with sane defaults
|
|
type = normalized_params["type"] || 0
|
|
mass_status = normalized_params["mass_status"] || 0
|
|
time_status = normalized_params["time_status"] || 0
|
|
ship_size_type = normalized_params["ship_size_type"] || 0
|
|
# Coerce to boolean; accept "true"/"false", 1/0, etc.
|
|
locked =
|
|
case normalized_params["locked"] do
|
|
val when val in [true, "true", 1, "1"] -> true
|
|
val when val in [false, "false", 0, "0"] -> false
|
|
nil -> false
|
|
other -> other # keep unknowns for caller-side validation
|
|
end
|
|
custom_info = normalized_params["custom_info"]
|
|
wormhole_type = normalized_params["wormhole_type"]
|
|
|
|
# Build standardized attrs map
|
|
attrs = %{
|
|
"solar_system_source" => src,
|
|
"solar_system_target" => tgt,
|
|
"type" => parse_optional_int(type, 0),
|
|
"mass_status" => parse_optional_int(mass_status, 0),
|
|
"time_status" => parse_optional_int(time_status, 0),
|
|
"ship_size_type" => parse_optional_int(ship_size_type, 0)
|
|
}
|
|
|
|
# Add non-nil optional attributes
|
|
attrs = if is_nil(locked), do: attrs, else: Map.put(attrs, "locked", locked)
|
|
attrs = if is_nil(custom_info), do: attrs, else: Map.put(attrs, "custom_info", custom_info)
|
|
attrs = if is_nil(wormhole_type), do: attrs, else: Map.put(attrs, "wormhole_type", wormhole_type)
|
|
|
|
{:ok, attrs}
|
|
else
|
|
{:error, msg} -> {:error, msg}
|
|
end
|
|
end
|
|
|
|
# Helper to handle various input formats
|
|
defp parse_to_int(nil, field), do: {:error, "Missing #{field}"}
|
|
defp parse_to_int(val, _field) when is_integer(val), do: {:ok, val}
|
|
defp parse_to_int(val, field) when is_binary(val) do
|
|
case Integer.parse(val) do
|
|
{i, ""} -> {:ok, i}
|
|
:error -> {:error, "Invalid #{field}: #{val}"}
|
|
_ -> {:error, "Invalid #{field}: #{val}"}
|
|
end
|
|
end
|
|
defp parse_to_int(val, field), do: {:error, "Invalid #{field} type: #{inspect(val)}"}
|
|
|
|
defp parse_optional_int(nil, default), do: default
|
|
defp parse_optional_int(i, _default) when is_integer(i), do: i
|
|
defp parse_optional_int(s, default) when is_binary(s) do
|
|
case Integer.parse(s) do
|
|
{i, _} -> i
|
|
:error -> default
|
|
end
|
|
end
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Standardized JSON Responses
|
|
# -----------------------------------------------------------------------------
|
|
|
|
@spec respond_data(Plug.Conn.t(), any(), atom() | integer()) :: Plug.Conn.t()
|
|
def respond_data(conn, data, status \\ :ok) do
|
|
conn
|
|
|> put_status(status)
|
|
|> json(%{data: data})
|
|
end
|
|
|
|
@spec error_response(Plug.Conn.t(), atom() | integer(), String.t(), map() | nil) :: Plug.Conn.t()
|
|
def error_response(conn, status, message, details \\ nil) do
|
|
body = if details, do: %{error: message, details: details}, else: %{error: message}
|
|
|
|
conn
|
|
|> put_status(status)
|
|
|> json(body)
|
|
end
|
|
|
|
@spec error_not_found(Plug.Conn.t(), String.t()) :: Plug.Conn.t()
|
|
def error_not_found(conn, message), do: error_response(conn, :not_found, message)
|
|
|
|
@doc """
|
|
Formats error messages for consistent display.
|
|
"""
|
|
@spec format_error(any()) :: String.t()
|
|
def format_error(error) when is_binary(error), do: error
|
|
def format_error(error) when is_atom(error), do: Atom.to_string(error)
|
|
def format_error(error), do: inspect(error)
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# JSON Serialization
|
|
# -----------------------------------------------------------------------------
|
|
|
|
@spec map_system_to_json(struct()) :: map()
|
|
def map_system_to_json(system) do
|
|
original = get_original_name(system.solar_system_id)
|
|
|
|
# Determine the actual custom_name: if name differs from original, use it as custom_name
|
|
actual_custom_name = if system.name != original and system.name not in [nil, ""], do: system.name, else: system.custom_name
|
|
|
|
base =
|
|
Map.take(system, ~w(
|
|
id map_id solar_system_id temporary_name description tag labels
|
|
locked visible status position_x position_y inserted_at updated_at
|
|
)a)
|
|
|> Map.put(:custom_name, actual_custom_name)
|
|
|
|
name = pick_name(system)
|
|
|
|
base
|
|
|> Map.put(:original_name, original)
|
|
|> Map.put(:name, name)
|
|
end
|
|
|
|
defp get_original_name(id) do
|
|
case MapSolarSystem.by_solar_system_id(id) do
|
|
{:ok, sys} -> sys.solar_system_name
|
|
_ -> "System #{id}"
|
|
end
|
|
end
|
|
|
|
defp pick_name(%{temporary_name: t, custom_name: c, name: n, solar_system_id: id} = system) do
|
|
original = get_original_name(id)
|
|
|
|
cond do
|
|
t not in [nil, ""] -> t
|
|
c not in [nil, ""] -> c
|
|
# If name differs from original, it's a custom name
|
|
n not in [nil, ""] and n != original -> n
|
|
true -> original
|
|
end
|
|
end
|
|
|
|
@spec connection_to_json(struct()) :: map()
|
|
def connection_to_json(conn) do
|
|
Map.take(conn, ~w(
|
|
id map_id solar_system_source solar_system_target mass_status
|
|
time_status ship_size_type type wormhole_type inserted_at updated_at
|
|
)a)
|
|
end
|
|
end
|