Skip to main content

lib/buble/apps.ex

defmodule Buble.Apps do
  @moduledoc """
  Preconfigured Buble app workflow methods.
  """

  alias Buble.Client
  alias Buble.Error

  @spec list(Client.t()) :: {:ok, map()} | {:error, Error.t()}
  def list(%Client{} = client), do: Client.request(client, :get, "/api/v1/apps")

  @spec list!(Client.t()) :: map()
  def list!(%Client{} = client), do: Buble.unwrap!(list(client))

  @spec retrieve(Client.t(), String.t()) :: {:ok, map()} | {:error, Error.t()}
  def retrieve(%Client{} = client, app_id) do
    Client.request(client, :get, "/api/v1/apps/#{Buble.HTTP.encode_path_segment(app_id)}")
  end

  @spec retrieve!(Client.t(), String.t()) :: map()
  def retrieve!(%Client{} = client, app_id), do: Buble.unwrap!(retrieve(client, app_id))
end

defmodule Buble.Apps.Generations do
  @moduledoc """
  App generation methods.
  """

  alias Buble.Client
  alias Buble.Error

  @terminal_statuses MapSet.new(~w[success failed canceled])

  @spec create(Client.t(), String.t(), keyword() | map()) :: {:ok, map()} | {:error, Error.t()}
  def create(%Client{} = client, app_id, params \\ %{}) do
    Client.request(
      client,
      :post,
      "/api/v1/apps/#{Buble.HTTP.encode_path_segment(app_id)}/generations",
      json: Buble.normalize_params(params)
    )
  end

  @spec create!(Client.t(), String.t(), keyword() | map()) :: map()
  def create!(%Client{} = client, app_id, params \\ %{}),
    do: Buble.unwrap!(create(client, app_id, params))

  @spec retrieve(Client.t(), String.t(), String.t()) :: {:ok, map()} | {:error, Error.t()}
  def retrieve(%Client{} = client, app_id, id) do
    Client.request(
      client,
      :get,
      "/api/v1/apps/#{Buble.HTTP.encode_path_segment(app_id)}/generations/#{Buble.HTTP.encode_path_segment(id)}"
    )
  end

  @spec retrieve!(Client.t(), String.t(), String.t()) :: map()
  def retrieve!(%Client{} = client, app_id, id), do: Buble.unwrap!(retrieve(client, app_id, id))

  @spec wait(Client.t(), String.t(), String.t(), keyword()) :: {:ok, map()} | {:error, Error.t()}
  def wait(%Client{} = client, app_id, id, opts \\ []) do
    interval = Keyword.get(opts, :interval, 2_000)
    timeout = Keyword.get(opts, :timeout, 600_000)
    throw_on_failed = Keyword.get(opts, :throw_on_failed, true)
    throw_on_canceled = Keyword.get(opts, :throw_on_canceled, true)
    deadline = System.monotonic_time(:millisecond) + timeout

    do_wait(client, app_id, id, interval, deadline, timeout, throw_on_failed, throw_on_canceled)
  end

  @spec wait!(Client.t(), String.t(), String.t(), keyword()) :: map()
  def wait!(%Client{} = client, app_id, id, opts \\ []),
    do: Buble.unwrap!(wait(client, app_id, id, opts))

  defp do_wait(
         client,
         app_id,
         id,
         interval,
         deadline,
         timeout,
         throw_on_failed,
         throw_on_canceled
       ) do
    case retrieve(client, app_id, id) do
      {:ok, envelope} ->
        task = response_data(envelope)
        status = task_status(task)

        if MapSet.member?(@terminal_statuses, status) do
          terminal_result(envelope, task, status, id, throw_on_failed, throw_on_canceled)
        else
          if System.monotonic_time(:millisecond) >= deadline do
            {:error,
             Error.new(
               :timeout,
               "App generation #{id} did not finish within #{timeout} milliseconds.",
               details: %{timeout: timeout}
             )}
          else
            Process.sleep(interval)

            do_wait(
              client,
              app_id,
              id,
              interval,
              deadline,
              timeout,
              throw_on_failed,
              throw_on_canceled
            )
          end
        end

      {:error, error} ->
        {:error, error}
    end
  end

  defp terminal_result(_envelope, task, "failed", _id, true, _throw_on_canceled) do
    message = get_in(task, ["error", "message"]) || "App generation failed."
    {:error, Error.new(:app_generation_failed, message, details: task, raw: task)}
  end

  defp terminal_result(_envelope, task, "canceled", id, _throw_on_failed, true) do
    {:error,
     Error.new(:app_generation_canceled, "App generation #{id} was canceled.",
       details: task,
       raw: task
     )}
  end

  defp terminal_result(envelope, _task, _status, _id, _throw_on_failed, _throw_on_canceled),
    do: {:ok, envelope}

  defp response_data(%{"data" => data}), do: data
  defp response_data(%{data: data}), do: data
  defp response_data(data), do: data

  defp task_status(%{"status" => status}), do: status
  defp task_status(%{status: status}), do: to_string(status)
  defp task_status(_task), do: nil
end