lib/data_for_seo/client.ex

defmodule DataForSeo.Client do
  @moduledoc """
  Provides basic and common functionalities for DataForSeo API based on Finch.
  """

  alias DataForSeo.Config

  def get(path, query_params \\ []) do
    url = request_url(path)

    get_request(url, query_params, headers())
  end

  def post(path, query_params \\ []) do
    url = request_url(path)
    # params = Jason.encode!(query_params)

    # |> Mojito.post(headers(), Jason.encode!(params))

    post_request(url, [], query_params, headers())
  end

  @doc """
  Generates basic Auth header from the given username and password

  ## Example

    iex> DataForSeo.Client.basic_auth_header("user", "hello")
    {"Authorization", "Basic dXNlcjpoZWxsbw=="}

  """
  def basic_auth_header(username, password) do
    auth64 = "#{username}:#{password}" |> Base.encode64()
    {"Authorization", "Basic #{auth64}"}
  end

  def get_request(url, query_params \\ [], headers \\ [], body \\ nil, opts \\ []) do
    :get
    |> build_request(url, query_params, headers, body)
    |> execute_request(opts)
  end

  def post_request(url, query_params \\ [], body \\ %{}, headers \\ [], opts \\ []) do
    body = Jason.encode!(body)

    :post
    |> build_request(url, query_params, headers, body)
    |> execute_request(opts)
  end

  def put_request(url, query_params \\ [], body \\ %{}, headers \\ [], opts \\ []) do
    body = Jason.encode!(body)

    :put
    |> build_request(url, query_params, headers, body)
    |> execute_request(opts)
  end

  def patch_request(url, query_params \\ [], body \\ %{}, headers \\ [], opts \\ []) do
    body = Jason.encode!(body)

    :patch
    |> build_request(url, query_params, headers, body)
    |> execute_request(opts)
  end

  def delete_request(url, query_params \\ [], headers \\ [], opts \\ []) do
    :delete
    |> build_request(url, query_params, headers, nil)
    |> execute_request(opts)
  end

  def validate_status_code(_ok_error, _ok_statuses \\ [200])

  def validate_status_code({:ok, response} = ok, ok_statuses) do
    if Enum.member?(ok_statuses, response.status) do
      ok
    else
      {:error, response}
    end
  end

  def validate_status_code({:error, _message} = error, _ok_statuses) do
    error
  end

  def decode_json_response(_response, _opts \\ [])

  def decode_json_response({:ok, %{body: body}}, opts) do
    case Jason.decode(body, opts) do
      {:ok, parsed_body} ->
        {:ok, parsed_body}

      {:error, %Jason.DecodeError{data: data}} ->
        {:error, data}
    end
  end

  def decode_json_response({:error, _message} = error, _opts) do
    error
  end

  # Helpers

  defp headers do
    config = Config.get_tuples() |> verify_config()

    [basic_auth_header(config[:login], config[:password]), {"content-type", "application/json"}]
  end

  defp request_url(path) do
    config = Config.get_tuples() |> verify_config()

    config[:base_url] <> path
  end

  defp verify_config([]) do
    raise DataForSeo.Error,
      message:
        "Config parameters are not set. Use DataForSeo.configure function to set parameters in advance."
  end

  defp verify_config(params), do: params

  defp build_request(method, url, query_params, headers, body) do
    url =
      url
      |> URI.parse()
      |> Map.put(:query, URI.encode_query(query_params))
      |> URI.to_string()

    Finch.build(method, url, headers, body)
  end

  defp execute_request(request, opts) do
    timeouts = timeout_options(opts)

    Finch.request(request, ApiFinch, timeouts)
  end

  defp timeout_options(opts) do
    config = Config.get_tuples() |> verify_config()

    receive_timeout = opts[:receive_timeout] || config[:receive_timeout] |> String.to_integer()
    pool_timeout = opts[:pool_timeout] || config[:pool_timeout] |> String.to_integer()

    Keyword.new(
      pool_timeout: pool_timeout,
      receive_timeout: receive_timeout
    )
  end
end