lib/graphql/utils.ex

defmodule BridgeEx.Graphql.Utils do
  @moduledoc """
  Misc utils for handling Graphql requests/responses.
  """

  require Logger

  @type client_response :: {:ok, any()} | {:error, any()}

  @type graphql_response ::
          {:error, String.t()}
          | {:ok, %{data: term()}}
          | {
              :ok,
              %{
                error: [
                  %{message: String.t(), locations: [%{line: integer(), column: integer()}]}
                ],
                data: term()
              }
            }

  @spec decode_http_response(
          {:ok, HTTPoison.Response.t() | HTTPoison.AsyncResponse.t()}
          | {:error, HTTPoison.Error.t()},
          String.t(),
          :strings | :atoms | :existing_atoms,
          Keyword.t()
        ) :: client_response()
  def decode_http_response(
        {:ok, %HTTPoison.Response{status_code: 200, body: body_string}},
        _,
        decode_keys,
        _
      ),
      do: decode_json(body_string, decode_keys)

  def decode_http_response(
        {:ok,
         %HTTPoison.Response{status_code: code, body: body_string, request_url: request_url}},
        query,
        _,
        log_options
      ) do
    log_response_on_error = Keyword.get(log_options, :log_response_on_error, false)
    log_query_on_error = Keyword.get(log_options, :log_query_on_error, false)

    metadata =
      [status_code: code, request_url: request_url]
      |> prepend_if(log_response_on_error, {:body_string, body_string})
      |> prepend_if(log_query_on_error, {:request_body, query})

    Logger.error("GraphQL: Bad Response error", metadata)

    {:error, {:bad_response, code}}
  end

  def decode_http_response(
        {:error, %HTTPoison.Error{reason: reason}},
        query,
        _,
        log_options
      ) do
    log_query_on_error = Keyword.get(log_options, :log_query_on_error, false)
    metadata = prepend_if([reason: inspect(reason)], log_query_on_error, {:request_body, query})
    Logger.error("GraphQL: HTTP error", metadata)

    {:error, {:http_error, reason}}
  end

  @spec parse_response(graphql_response()) :: client_response()
  def parse_response({:error, error}), do: {:error, error}

  def parse_response({:ok, %{errors: errors}}), do: {:error, errors}
  def parse_response({:ok, %{"errors" => errors}}), do: {:error, errors}

  def parse_response({:ok, %{data: data}}), do: {:ok, data}
  def parse_response({:ok, %{"data" => data}}), do: {:ok, data}

  defp prepend_if(list, false, _), do: list
  defp prepend_if(list, true, value), do: [value | list]

  @spec decode_json(String.t(), :strings | :atoms | :existing_atoms) ::
          {:ok, map()} | {:error, any()}
  defp decode_json(body, :strings), do: Jason.decode(body)
  defp decode_json(body, :atoms), do: Jason.decode(body, keys: :atoms)
  defp decode_json(body, :existing_atoms), do: Jason.decode(body, keys: :atoms!)
end