aprs.me/lib/aprsme_web/controllers/api/v1/weather_controller.ex
Graham McIntire b8a9b8a465
chore(dialyzer): enable stricter flags and fix 97 resulting findings
Turned on :error_handling, :underspecs, and :unmatched_returns in
mix.exs dialyzer config. The 97 warnings this surfaced were fixed in
place rather than suppressed:

- unmatched_return (79): explicit discard with `_ = ...` for
  fire-and-forget side effects (Process.cancel_timer, :ets.new,
  send/2), and pattern-matched `:ok = ...` for control-plane
  Phoenix.PubSub subscribe/unsubscribe/broadcast calls so a future
  return-shape change fails loud.

- contract_supertype (18): tightened @spec arg and return types on
  data_builder, historical_loader, url_params, packet_utils,
  encoding_utils, aprs_symbol, weather_controller, packet_replay to
  match each function's actual success typing.

No behavioural change. mix compile clean, 1008 tests pass, dialyzer
count is now 0.
2026-04-21 10:07:01 -05:00

156 lines
5.2 KiB
Elixir

defmodule AprsmeWeb.Api.V1.WeatherController do
@moduledoc """
Controller for weather-related API endpoints.
"""
use AprsmeWeb, :controller
alias Aprsme.Packets.PreparedQueries
action_fallback AprsmeWeb.Api.V1.FallbackController
@doc """
Returns nearby weather stations within a specified radius.
## Parameters
* `lat` - Latitude (-90 to 90) - required
* `lon` - Longitude (-180 to 180) - required
* `radius` - Search radius in miles (> 0, max 1000) - required
* `hours` - Hours of data to retrieve (1-168, default: 6) - optional
* `limit` - Maximum number of results (1-100, default: 50) - optional
## Examples
GET /api/v1/weather/nearby?lat=37.7749&lon=-122.4194&radius=10
GET /api/v1/weather/nearby?lat=37.7749&lon=-122.4194&radius=10&hours=24&limit=25
"""
def nearby(conn, params) do
with {:ok, validated_params} <- validate_params(params) do
stations =
PreparedQueries.get_nearby_weather_stations(
validated_params.lat,
validated_params.lon,
validated_params.radius,
hours: validated_params.hours,
limit: validated_params.limit
)
# Transform params for JSON response
response_params = %{
latitude: validated_params.lat,
longitude: validated_params.lon,
radius_miles: validated_params.radius,
hours: validated_params.hours,
limit: validated_params.limit
}
render(conn, :nearby,
stations: stations,
params: response_params
)
end
end
# Private Functions
@spec validate_params(map()) ::
{:ok,
%{
lat: float(),
lon: float(),
radius: float(),
hours: integer(),
limit: integer()
}}
| {:error, :bad_request, String.t()}
| {:error, :unprocessable_entity, String.t()}
defp validate_params(params) do
with {:ok, lat} <- validate_required_float(params, "lat"),
{:ok, lon} <- validate_required_float(params, "lon"),
{:ok, radius} <- validate_required_float(params, "radius"),
{:ok, hours} <- validate_optional_integer(params, "hours", 6),
{:ok, limit} <- validate_optional_integer(params, "limit", 50),
:ok <- validate_latitude(lat),
:ok <- validate_longitude(lon),
:ok <- validate_radius(radius),
:ok <- validate_hours(hours),
:ok <- validate_limit(limit) do
{:ok,
%{
lat: lat,
lon: lon,
radius: radius,
hours: hours,
limit: limit
}}
end
end
@spec validate_required_float(map(), String.t()) ::
{:ok, float()} | {:error, :bad_request, String.t()}
defp validate_required_float(params, key) do
case Map.get(params, key) do
nil ->
{:error, :bad_request, "Missing required parameter: #{key}"}
value when is_binary(value) ->
case Float.parse(value) do
{float_value, ""} -> {:ok, float_value}
_ -> {:error, :bad_request, "Invalid numeric value for parameter: #{key}"}
end
value when is_number(value) ->
{:ok, value / 1}
end
end
@spec validate_optional_integer(map(), <<_::40>>, 6 | 50) ::
{:ok, integer()} | {:error, :bad_request, <<_::64, _::_*8>>}
defp validate_optional_integer(params, key, default) do
case Map.get(params, key) do
nil ->
{:ok, default}
value when is_binary(value) ->
case Integer.parse(value) do
{int_value, ""} -> {:ok, int_value}
_ -> {:error, :bad_request, "Invalid integer value for parameter: #{key}"}
end
value when is_integer(value) ->
{:ok, value}
value when is_float(value) ->
{:ok, trunc(value)}
end
end
@spec validate_latitude(float()) :: :ok | {:error, :unprocessable_entity, String.t()}
defp validate_latitude(lat) when lat >= -90 and lat <= 90, do: :ok
defp validate_latitude(_lat), do: {:error, :unprocessable_entity, "Invalid latitude: must be between -90 and 90"}
@spec validate_longitude(float()) :: :ok | {:error, :unprocessable_entity, String.t()}
defp validate_longitude(lon) when lon >= -180 and lon <= 180, do: :ok
defp validate_longitude(_lon), do: {:error, :unprocessable_entity, "Invalid longitude: must be between -180 and 180"}
@spec validate_radius(float()) :: :ok | {:error, :unprocessable_entity, String.t()}
defp validate_radius(radius) when radius > 0 and radius <= 1000, do: :ok
defp validate_radius(radius) when radius <= 0,
do: {:error, :unprocessable_entity, "Invalid radius: must be greater than 0"}
defp validate_radius(_radius), do: {:error, :unprocessable_entity, "Invalid radius: must not exceed 1000 miles"}
@spec validate_hours(integer()) :: :ok | {:error, :unprocessable_entity, String.t()}
defp validate_hours(hours) when hours >= 1 and hours <= 168, do: :ok
defp validate_hours(_hours), do: {:error, :unprocessable_entity, "Invalid hours: must be between 1 and 168"}
@spec validate_limit(integer()) :: :ok | {:error, :unprocessable_entity, String.t()}
defp validate_limit(limit) when limit >= 1 and limit <= 100, do: :ok
defp validate_limit(_limit), do: {:error, :unprocessable_entity, "Invalid limit: must be between 1 and 100"}
end