lib/noaa/observations/station.ex

defmodule NOAA.Observations.Station do
  @moduledoc """
  Fetches the latest observation for a given NOAA station.
  """

  alias NOAA.Observations.{Log, Message, State, URLTemplates}

  @typedoc "Station ID"
  @type id :: String.t()
  @typedoc "Station name"
  @type name :: String.t()
  @typedoc "NOAA weather observation"
  @type observation :: map
  @typedoc "NOAA station"
  @type t :: {id, name}

  @doc """
  Fetches the latest observation for a given NOAA `station`.

  Returns either tuple `{:ok, observation}` or tuple `{:error, text}`.

  ## Parameters

    - `{id, name}`    - NOAA station
    - `url_templates` - URL templates

  ## Examples

      iex> alias NOAA.Observations.Station
      iex> url_templates = [
      ...>   station:
      ...>     "https://w1.weather.gov/xml/current_obs/display.php?stid=" <>
      ...>       "<%=station%>"
      ...> ]
      iex> {:ok, observation} =
      ...>   Station.observation({"KFSO", "KFSO name"}, "vt", url_templates)
      iex> is_map(observation) and
      ...> is_binary(observation["temp_c"]) and
      ...> is_binary(observation["wind_mph"])
      true

      iex> alias NOAA.Observations.Station
      iex> url_templates = [
      ...>   station:
      ...>     "htp://w1.weather.gov/xml/current_obs/display.php?stid=" <>
      ...>       "<%=station%>"
      ...> ]
      iex> {:error, text} =
      ...>   Station.observation({"KFSO", "KFSO name"}, "vt", url_templates)
      iex> text
      "reason => :nxdomain"

      iex> alias NOAA.Observations.Station
      iex> url_templates = [
      ...>   station:
      ...>     "https://w1.weather.gov/xml/past_obs/display.php?stid=" <>
      ...>       "<%=station%>"
      ...> ]
      iex> {:error, text} =
      ...>   Station.observation({"KFSO", "KFSO name"}, "vt", url_templates)
      iex> text
      "status code 404 ⇒ Not Found"
  """
  @spec observation(t, State.code, Keyword.t()) ::
          {:ok, observation} | {:error, String.t()}
  def observation({id, name} = _station, code, url_templates) do
    url = URLTemplates.url(url_templates, station: id)
    :ok = Log.info(:fetching_observation, {id, name, code, url, __ENV__})

    case HTTPoison.get(url) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        {
          :ok,
          # <weather>Fog</weather>
          ~r{<([^/][^>]+)>(.*?)</\1>} # capture XML tag and value
          |> Regex.scan(body, capture: :all_but_first) # i.e. only subpatterns
          |> Map.new(&List.to_tuple/1) # each [tag, value] -> {tag, value}
        }

      {:ok, %HTTPoison.Response{status_code: code}} ->
        {:error, Message.status(code)}

      {:error, %HTTPoison.Error{reason: reason}} ->
        {:error, Message.error(reason)}
    end
  end
end