lib/ex_typesense/http_client.ex

defmodule ExTypesense.HttpClient do
  @moduledoc since: "0.1.0"
  @moduledoc """
  Http client for Typesense server.
  """

  @type request_body() :: iodata() | nil
  @type request_method() :: :get | :post | :delete | :patch | :put
  @type request_path() :: String.t()
  @api_header_name 'X-TYPESENSE-API-KEY'

  def get_host, do: Application.get_env(:ex_typesense, :host)
  def get_port, do: Application.get_env(:ex_typesense, :port)
  def get_scheme, do: Application.get_env(:ex_typesense, :scheme)
  def api_key, do: Application.get_env(:ex_typesense, :api_key)

  @doc """
  Req client.

  ## Examples
      iex> HttpClient.run(:get, "/collections")
      {:ok,
        [%{
          "created_at" => 123456789,
          "default_sorting_field" => "num_employees",
          "fields" => [...],
          "name" => "companies",
          "num_documents" => 0,
          "symbols_to_index" => [],
          "token_separators" => []
        }]
      }
  """
  @doc since: "0.1.0"
  @spec run(request_method(), request_path(), request_body(), map()) ::
          {:ok, map()} | {:error, map()}
  def run(request_method, request_path, body \\ nil, query \\ %{}) do
    url = %URI{
      scheme: get_scheme() || "https",
      host: get_host(),
      port: get_port() || 443,
      path: request_path,
      query: URI.encode_query(query)
    }

    response =
      %Req.Request{
        body: body,
        method: request_method,
        url: url
      }
      |> Req.Request.put_header("X-TYPESENSE-API-KEY", api_key())
      |> Req.Request.append_error_steps(retry: &Req.Steps.retry/1)
      |> Req.Steps.encode_body()
      |> Req.Request.run!()

    case response.status in 200..299 do
      true ->
        {:ok, Jason.decode!(response.body)}

      false ->
        {:error, Jason.decode!(response.body)["message"]}
    end
  end

  @spec httpc_run(URI.__struct__(), atom(), String.t(), list()) :: {:ok, map()} | {:error, map()}
  def httpc_run(uri, method, payload, content_type \\ 'application/json') do
    uri = %URI{
      scheme: get_scheme(),
      host: get_host(),
      port: get_port(),
      path: uri.path,
      query: uri.query
    }

    api_key = String.to_charlist(api_key())

    headers = [{@api_header_name, api_key}]

    request = {
      URI.to_string(uri),
      headers,
      content_type,
      payload
    }

    :ok = :ssl.start()

    http_opts = [
      ssl: [
        {:versions, [:"tlsv1.2"]},
        verify: :verify_peer,
        cacerts: :public_key.cacerts_get(),
        customize_hostname_check: [
          match_fun: :public_key.pkix_verify_hostname_match_fun(:https)
        ]
      ],
      timeout: 3_600,
      connect_timeout: 3_600
    ]

    case :httpc.request(method, request, http_opts, []) do
      {:ok, {_status_code, _headers, message}} ->
        case Jason.decode(message) do
          {:ok, message} ->
            {:ok, message}

          {:error, %Jason.DecodeError{data: data}} ->
            message =
              data
              |> String.split("\n", trim: true)
              |> Stream.map(&Jason.decode!/1)
              |> Enum.to_list()

            {:ok, message}
        end

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