aprs.me/lib/aprsme_web/controllers/api/v1/fallback_controller.ex
Graham McIntire a796f8a3a9
Add Weather API controller with parameter validation
Implements GET /api/v1/weather/nearby endpoint with comprehensive parameter
validation and error handling. The controller validates required parameters
(lat, lon, radius) and optional parameters (hours, limit) with appropriate
range checking. Integrates with PreparedQueries for efficient database access
and WeatherJSON for response serialization.

Key features:
- Required params: lat (-90 to 90), lon (-180 to 180), radius (0-1000 miles)
- Optional params: hours (1-168, default 6), limit (1-100, default 50)
- Proper error responses via FallbackController (400 for bad requests, 422 for validation)
- Full test coverage (16 tests) including edge cases and error scenarios

Also updates FallbackController to handle {:error, status, message} tuples
and fixes WeatherJSON to support both field name formats (lat/lon and latitude/longitude)
for compatibility with existing tests and actual query results.
2026-03-22 11:27:45 -05:00

81 lines
2.5 KiB
Elixir

defmodule AprsmeWeb.Api.V1.FallbackController do
@moduledoc """
Translates controller action results into valid `Plug.Conn` responses.
See `Phoenix.Controller.action_fallback/1` for more details.
"""
use AprsmeWeb, :controller
use Gettext, backend: AprsmeWeb.Gettext
alias AprsmeWeb.Api.V1.ErrorJSON
# This clause handles errors returned by Ecto's insert/update/delete.
def call(conn, {:error, %Ecto.Changeset{} = changeset}) do
conn
|> put_status(:unprocessable_entity)
|> put_view(json: AprsmeWeb.Api.V1.ChangesetJSON)
|> render(:error, changeset: changeset)
end
# This clause is an example of how to handle resources that cannot be found.
def call(conn, {:error, :not_found}) do
conn
|> put_status(:not_found)
|> put_view(json: ErrorJSON)
|> render(:"404")
end
# Handle errors with status and message (e.g., {:error, :bad_request, "message"})
def call(conn, {:error, status, message}) when is_atom(status) and is_binary(message) do
conn
|> put_status(status)
|> put_view(json: ErrorJSON)
|> render(:error, message: message)
end
# Handle generic errors
def call(conn, {:error, reason}) when is_atom(reason) do
status =
case reason do
:unauthorized -> :unauthorized
:forbidden -> :forbidden
:bad_request -> :bad_request
_ -> :internal_server_error
end
conn
|> put_status(status)
|> put_view(json: ErrorJSON)
|> render(:error, message: format_error_message(reason))
end
# Handle string error messages
def call(conn, {:error, message}) when is_binary(message) do
conn
|> put_status(:bad_request)
|> put_view(json: ErrorJSON)
|> render(:error, message: message)
end
# Handle timeout errors
def call(conn, :timeout) do
conn
|> put_status(:request_timeout)
|> put_view(json: ErrorJSON)
|> render(:error, message: gettext("Request timeout"))
end
# Default fallback for unexpected errors
def call(conn, _error) do
conn
|> put_status(:internal_server_error)
|> put_view(json: ErrorJSON)
|> render(:"500")
end
defp format_error_message(:not_found), do: gettext("Resource not found")
defp format_error_message(:unauthorized), do: gettext("Unauthorized access")
defp format_error_message(:forbidden), do: gettext("Access forbidden")
defp format_error_message(:bad_request), do: gettext("Bad request")
defp format_error_message(reason), do: gettext("An error occurred: %{reason}", reason: reason)
end