lib/hui/http.ex

defmodule Hui.Http do
  @moduledoc """
  A struct and functions for making Solr HTTP request and response.
  """

  alias Hui.Encoder
  alias Hui.Http
  alias Hui.Query
  alias Hui.Query.Update
  alias Hui.Utils.ParserType
  alias Hui.Utils.Url, as: UrlUtils

  import Hui.Http.Client

  @json_parser Application.compile_env(:hui, :json_parser)
  @parser_not_configured ParserType.not_configured()

  defstruct body: nil,
            client: impl(),
            headers: [],
            method: :get,
            options: [],
            response_parser: nil,
            status: nil,
            url: ""

  @type querying_struct :: Query.Standard.t() | Query.Common.t() | Query.DisMax.t()
  @type faceting_struct :: Query.Facet.t() | Query.FacetRange.t() | Query.FacetInterval.t()
  @type highlighting_struct ::
          Query.Highlight.t()
          | Query.HighlighterUnified.t()
          | Query.HighlighterOriginal.t()
          | Query.HighlighterFastVector.t()

  @type misc_struct :: Query.MoreLikeThis.t() | Query.Suggest.t() | Query.SpellCheck.t() | Query.Metrics.t()
  @type solr_struct :: querying_struct | faceting_struct | highlighting_struct | misc_struct

  @type query :: keyword | map | solr_struct | [solr_struct]
  @type update_query :: binary | map | list(map) | Update.t()

  @type client :: module
  @type method :: :get | :post

  @type url :: binary() | atom()
  @type headers :: list()
  @type options :: list()

  @type endpoint :: url | {url, headers} | {url, headers, options}

  @type body :: nil | iodata() | map()
  @type request_url :: iodata()
  @type response :: {:ok, t} | {:error, Hui.Error.t()}

  @typedoc """
  The main request and response data struct.
  """
  @type t :: %__MODULE__{
          body: body,
          client: module(),
          headers: list(),
          method: :get | :post,
          options: keyword(),
          response_parser: module(),
          status: nil | integer(),
          url: request_url
        }

  def new(:get, endpoint, query, client) do
    with {:ok, {url, headers, options, opted_parser}} <- UrlUtils.parse_endpoint(endpoint),
         parser <- maybe_infer_parser(query, opted_parser) do
      %Http{
        client: client,
        url: [url, "?", Encoder.encode(query)],
        headers: headers,
        method: :get,
        options: options,
        response_parser: parser
      }
    end
  end

  def new(:post, endpoint, updates, client) do
    with {:ok, {url, headers, options, parser}} <- UrlUtils.parse_endpoint(endpoint),
         updates <- maybe_encode_updates(updates) do
      parser = if parser == :not_configured, do: @json_parser, else: parser

      %Http{
        body: updates,
        client: client,
        url: url,
        headers: headers,
        method: :post,
        options: options,
        response_parser: parser
      }
    end
  end

  defp maybe_infer_parser(query, opted_parser) do
    case opted_parser do
      parser when parser == @parser_not_configured -> ParserType.infer(query)
      opted_parser -> opted_parser
    end
  end

  defp maybe_encode_updates(updates) when is_binary(updates), do: updates
  defp maybe_encode_updates(updates), do: Encoder.encode(updates)

  @doc false
  @spec get(endpoint, query, module) :: response
  def get(endpoint, query, client \\ impl()) do
    new(:get, endpoint, query, client) |> request()
  end

  @doc false
  @spec post(endpoint, update_query, boolean, module) :: response
  def post(endpoint, updates, commit \\ true, client \\ impl())

  def post(endpoint, updates, _commit, client) when is_binary(updates), do: do_post(endpoint, updates, client)
  def post(endpoint, %Update{} = updates, _commit, client), do: do_post(endpoint, updates, client)

  def post(endpoint, %{} = doc, commit, client), do: post(endpoint, %Update{doc: doc, commit: commit}, client)

  def post(endpoint, [%{} = _doc | _] = docs, commit, client) do
    post(endpoint, %Update{doc: docs, commit: commit}, client)
  end

  defp do_post(endpoint, updates, client), do: new(:post, endpoint, updates, client) |> request()

  defp request(%Http{} = req) do
    req
    |> dispatch()
    |> handle_response(req)
  end
end