defmodule AprsmeWeb.Api.V1.WeatherController do @moduledoc """ Controller for weather-related API endpoints. """ use AprsmeWeb, :controller alias Aprsme.ApiMetrics alias Aprsme.Packets.PreparedQueries alias Aprsme.PlausibleAnalytics 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 PlausibleAnalytics.track(conn, "api_weather_nearby") ApiMetrics.track(%{}, "api_weather_nearby") 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