lib/noaa/observations/state.ex

defmodule NOAA.Observations.State do
  @moduledoc """
  Fetches the stations for a US state/territory code.
  """

  use PersistConfig

  alias NOAA.Observations.{Log, Message, Station, TemplatesAgent}

  @typedoc "US state/territory code"
  @type code :: <<_::16>>
  @typedoc "Erroneous state"
  @type error :: map

  @doc """
  Fetches the stations for a `state_code`.

  Returns a tuple of either `{:ok, [station]}` or `{:error, error_code, text}`.

  ## Parameters

    - `state_code` - US state/territory code

  ## Examples

      iex> alias NOAA.Observations.{State, TemplatesAgent}
      iex> :ok = TemplatesAgent.refresh()
      iex> {:ok, stations} = State.stations("VT")
      iex> %{"KFSO" => name} = Map.new(stations)
      iex> name
      "Franklin County State Airport"

      iex> alias NOAA.Observations.{State, TemplatesAgent}
      iex> template =
      ...>   "http://forecast.weather.gov/xml/current_obs" <>
      ...>     "/seek.php?state=<%=state_code%>&Find=Find"
      iex> TemplatesAgent.update_state_template(template)
      iex> State.stations("VT")
      {:error, 301, "Moved Permanently"}

      iex> alias NOAA.Observations.{State, TemplatesAgent}
      iex> template =
      ...>   "https://www.weather.gov/xml/current_obs" <>
      ...>     "/seek.php?state=<%=state_code%>&Find=Find"
      iex> TemplatesAgent.update_state_template(template)
      iex> State.stations("VT")
      {:error, 302, "Found (Moved Temporarily)"}

      iex> alias NOAA.Observations.{State, TemplatesAgent}
      iex> template =
      ...>   "https://forecast.weather.gov/xml/past_obs" <>
      ...>     "/seek.php?state=<%=state_code%>&Find=Find"
      iex> TemplatesAgent.update_state_template(template)
      iex> State.stations("VT")
      {:error, 404, "Not Found"}

      iex> alias NOAA.Observations.{State, TemplatesAgent}
      iex> template =
      ...>   "htp://forecast.weather.gov/xml/current_obs" <>
      ...>     "/seek.php?state=<%=state_code%>&Find=Find"
      iex> TemplatesAgent.update_state_template(template)
      iex> State.stations("VT")
      {:error, :nxdomain, "Non-Existent Domain"}

      iex> alias NOAA.Observations.{State, TemplatesAgent}
      iex> template = "http://localhost:65535"
      iex> TemplatesAgent.update_state_template(template)
      iex> State.stations("VT")
      {:error, :econnrefused, "Connection Refused By Server"}

      iex> alias NOAA.Observations.{State, TemplatesAgent}
      iex> template = "http://localhost:65536"
      iex> TemplatesAgent.update_state_template(template)
      iex> State.stations("VT")
      {:error, :checkout_failure, "Checkout Failure"}
  """
  @spec stations(code) :: {:ok, [Station.t()]} | {:error, any, String.t()}
  def stations(state_code) do
    state_url = TemplatesAgent.state_url(state_code: state_code)

    case HTTPoison.get(state_url) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        :ok = Log.info(:stations_fetched, {state_code, state_url, __ENV__})

        {
          :ok,
          # <a href="/xml/current_obs/display.php?stid=KTIX">Titusville</a>
          # capture station ID and name
          ~r[<a href=".*?stid=(.*?)">(.*?)</a>]
          # i.e. only subpatterns
          |> Regex.scan(body, capture: :all_but_first)
          # each [id, name] -> {id, name}
          |> Enum.map(&List.to_tuple/1)
        }

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

        :ok =
          Log.error(
            :stations_not_fetched,
            {state_code, state_url, status_code, error_text, __ENV__}
          )

        {:error, status_code, error_text}

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

        :ok =
          Log.error(
            :stations_not_fetched,
            {state_code, state_url, reason, error_text, __ENV__}
          )

        {:error, reason, error_text}
    end
  end
end