lib/forecastr.ex

defmodule Forecastr do
  @moduledoc """
  Forecastr is an application that queries the Open Weather Map API

  The Forecastr user API is exposed in this way:

  # Query the backend weather API for today's weather
  Forecastr.forecast(:today, query, renderer: Forecastr.Renderer.ASCII )

  # Query the backend weather API for the forecast in the next days
  Forecastr.forecast(:next_days, query, renderer: Forecastr.Renderer.ASCII )

  For example:

  Forecastr.forecast(:today, "Berlin")

  Forecastr.forecast(:next_days, "Berlin", units: :imperial)

  Forecastr.forecast(:today, "Lima", units: :imperial, renderer: Forecastr.Renderer.PNG)
  """

  @doc """
  Forecastr.forecast(:today, "Berlin")

  Forecastr.forecast(:today, "Mexico city", renderer: Forecastr.Renderer.ANSI)
  """
  @type renderer ::
          Forecastr.Renderer.ASCII
          | Forecastr.Renderer.ANSI
          | Forecastr.Renderer.HTML
          | Forecastr.Renderer.JSON
          | Forecastr.Renderer.PNG
  @type when_to_forecast :: :today | :next_days
  @spec forecast(when_to_forecast, query :: String.t(), params :: Keyword.t()) ::
          {:ok, binary()} | {:ok, list(binary())} | {:error, atom()}
  def forecast(
        when_to_forecast,
        query,
        params \\ [units: :metric]
      )

  def forecast(_when_to_forecast, "", _params), do: {:error, :not_found}

  def forecast(when_to_forecast, query, params) do
    location = String.downcase(query)
    renderer = Keyword.get(params, :renderer, Forecastr.Renderer.ASCII)
    units = Keyword.get(params, :units, :metric)

    with {:ok, response} <- perform_query(location, when_to_forecast, params) do
      {:ok, renderer.render(response, units)}
    end
  end

  defp perform_query(query, when_to_forecast, params) do
    with {:ok, :miss} <- fetch_from_cache(when_to_forecast, query),
         {:ok, response} <- fetch_from_backend(when_to_forecast, query, params),
         :ok <- Forecastr.Cache.set(when_to_forecast, query, response) do
      {:ok, response}
    else
      {:ok, _response} = response -> response
      {:error, _} = error -> error
    end
  end

  defp fetch_from_cache(when_to_forecast, query) do
    case Forecastr.Cache.get(when_to_forecast, query) do
      nil -> {:ok, :miss}
      response -> {:ok, response}
    end
  end

  defp fetch_from_backend(when_to_forecast, query, params) do
    backend = Application.get_env(:forecastr, :backend)

    with {:ok, weather} <- backend.weather(when_to_forecast, query, params),
         {:ok, normalized_weather} <- backend.normalize(weather) do
      {:ok, normalized_weather}
    end
  end
end