lib/live_rss/pool.ex

defmodule LiveRSS.Pool do
  @moduledoc """
  LiveRSS.Pool is a GenServer that pools a RSS feed periodically.

  ```elixir
  LiveRSS.Pool.start_link(name: :live_rss_blog, url: "https://blog.test/feed.rss", refresh_every: :timer.hours(2))
  LiveRSS.Pool.start_link(name: :live_rss_videos, url: "https://videos.test/feed.rss", refresh_every: :timer.hours(1))
  LiveRSS.Pool.start_link(name: :live_rss_photos, url: "https://photos.test/feed.rss", refresh_every: :timer.minutes(10))

  %FeederEx.Feed{} = LiveRSS.get(:live_rss_blog)
  ```

  Use `LiveRSS.Pool.start_link/1` to start the GenServer. You can use the following
  options as the example:
  * `name`: the atom name of the process that will be used to retrieve the feed later
  * `url`: the URL of the RSS feed
  * `refresh_every`: the frequency the feed will be fetched by the GenServer

  You can use `LiveRSS.get/1` to retrieve the feed as a `%FeederEx.Feed{}` struct.
  """

  require Logger
  use GenServer

  def start_link(opts) do
    with :ok <- validate_uri(opts),
         {:ok, name} <- validate_name(opts),
         {:ok, pid} <- GenServer.start_link(__MODULE__, opts, name: name),
         do: {:ok, pid}
  end

  defp validate_name(opts) do
    case opts[:name] do
      nil -> {:error, :invalid_name}
      name when is_atom(name) -> {:ok, name}
      _any -> {:error, :invalid_name}
    end
  end

  defp validate_uri(opts) do
    with url when is_binary(url) <- opts[:url],
         %URI{} = uri <- URI.parse(url),
         %URI{} = uri when is_binary(uri.scheme) and is_binary(uri.host) and is_binary(uri.path) <-
           uri do
      :ok
    else
      _any -> {:error, :invalid_url}
    end
  end

  @doc """
  Returns a `%FeederEx.Feed{}`. If the feed fails to be fetched, it returns nil and logs
  error.
  """
  @spec get(atom()) :: %FeederEx.Feed{} | nil
  def get(process_name) do
    process_name
    |> Process.whereis()
    |> GenServer.call(:get_feed)
  end

  @default_state [refresh_every: :timer.hours(1), url: nil, feed: nil]

  @impl true
  def init(state) do
    Logger.info("LiveRSS: Started #{state[:name]} pooling every #{state[:refresh_every]}ms")

    state = Keyword.merge(@default_state, state)
    schedule_pooling(state)

    {:ok, state}
  end

  @impl true
  def handle_call(:get_feed, _from, state) do
    case state[:feed] do
      %FeederEx.Feed{} = feed ->
        {:reply, feed, state}

      nil ->
        state = put_feed(state)
        {:reply, state[:feed], state}
    end
  end

  @impl true
  def handle_info(:pool, state) do
    state = put_feed(state)
    schedule_pooling(state)

    {:noreply, state}
  end

  defp schedule_pooling(state) do
    Process.send_after(self(), :pool, state[:refresh_every])
  end

  defp put_feed(state) do
    case LiveRSS.HTTP.get(state[:url]) do
      {:ok, %FeederEx.Feed{} = feed} ->
        Logger.info("LiveRSS: Updated #{state[:name]} data")
        Keyword.put(state, :feed, feed)

      _any ->
        state
    end
  end
end