lib/elixir_google_spreadsheets/registry.ex

defmodule GSS.Registry do
  @moduledoc """
  Google spreadsheets core authorization.
  Automatically updates access token after expiration.
  """

  use GenServer

  @typedoc """
  State of Google Cloud API :
      %{
          auth: %Goth.Token{
              expires: 1453356568,
              token: "ya29.cALlJ4HHWRvMkYB-WsAR-CZnexE459yA7QPqKg3nei1y2T7-iqmbcgxb8XrTATNn_Blim",
              type: "Bearer"
          }
      }
  """
  @type state :: map()

  @auth_scope "https://www.googleapis.com/auth/spreadsheets"

  @spec start_link(any()) :: {:ok, pid}
  def start_link(_args \\ []) do
    initial_state = %{
      active_sheets: %{}
    }

    GenServer.start_link(__MODULE__, initial_state, name: __MODULE__)
  end

  @spec init(state) :: {:ok, state}
  def init(state) do
    {:ok, state}
  end

  @doc """
  Get account authorization token.
  """
  @spec token() :: String.t()
  def token do
    GenServer.call(__MODULE__, :token)
  end

  @doc """
  Add or replace Google Spreadsheet in a registry.
  """
  @spec new_spreadsheet(String.t(), pid, Keyword.t()) :: :ok
  def new_spreadsheet(spreadsheet_id, pid, opts \\ []) do
    GenServer.call(__MODULE__, {:new_spreadsheet, spreadsheet_id, pid, opts})
  end

  @doc """
  Fetch Google Spreadsheet proccess by it's id in the registry.
  """
  @spec spreadsheet_pid(String.t(), Keyword.t()) :: pid | nil
  def spreadsheet_pid(spreadsheet_id, opts \\ []) do
    GenServer.call(__MODULE__, {:spreadsheet_pid, spreadsheet_id, opts})
  end

  # Get account authorization token, issue new token in case old has expired.
  def handle_call(
        :token,
        _from,
        %{
          auth: %{
            token: token,
            expires: expires
          }
        } = state
      ) do
    if expires < :os.system_time(:seconds) do
      new_state = Map.put(state, :auth, refresh_token())
      {:reply, new_state.auth.token, new_state}
    else
      {:reply, token, state}
    end
  end

  def handle_call(:token, _from, state) do
    new_state = Map.put(state, :auth, refresh_token())
    {:reply, new_state.auth.token, new_state}
  end

  # Update :active_sheets registry record.
  def handle_call(
        {:new_spreadsheet, spreadsheet_id, pid, opts},
        _from,
        %{active_sheets: active_sheets} = state
      )
      when is_bitstring(spreadsheet_id) and is_pid(pid) do
    registry_id = id(spreadsheet_id, opts)
    new_active_sheets = Map.put(active_sheets, registry_id, pid)
    new_state = Map.put(state, :active_sheets, new_active_sheets)
    {:reply, :ok, new_state}
  end

  # Get pid of sheet in :active_sheets registry.
  def handle_call(
        {:spreadsheet_pid, spreadsheet_id, opts},
        _from,
        %{active_sheets: active_sheets} = state
      )
      when is_bitstring(spreadsheet_id) do
    registry_id = id(spreadsheet_id, opts)
    {:reply, Map.get(active_sheets, registry_id, nil), state}
  end

  @spec refresh_token() :: map()
  defp refresh_token do
    {:ok, token} = Goth.Token.for_scope(@auth_scope)
    token
  end

  @spec id(String.t(), Keyword.t()) :: String.t()
  defp id(spreadsheet_id, opts) do
    list_name = Keyword.get(opts, :list_name)

    if is_bitstring(list_name) do
      "#{spreadsheet_id}!#{list_name}"
    else
      spreadsheet_id
    end
  end
end