lib/snap/cluster.ex

defmodule Snap.Cluster do
  @moduledoc """
  Defines a cluster.

  A cluster maps to an Elasticsearch endpoint.

  When used, the cluster expects `:otp_app` as an option. The `:otp_app`
  should point to an OTP application that has the cluster configuration. For
  example, this cluster:

  ```
  defmodule MyApp.Cluster do
    use Snap.Cluster, otp_app: :my_app
  end
  ```

  Can be configured with:

  ```
  config :my_app, MyApp.Cluster,
    url: "http://localhost:9200",
    username: "username",
    password: "password",
    index_namespace: "app-dev"
  ```

  For details about how index namespacing works, see `Snap.Cluster.Namespace`.
  """
  defmacro __using__(opts) do
    quote do
      alias Snap.Cluster.Supervisor
      alias Snap.Request

      def init(config) do
        {:ok, config}
      end

      defoverridable init: 1

      @doc """
      Returns the config map that the Cluster was defined with.
      """
      def config() do
        Supervisor.config(__MODULE__)
      end

      @doc """
      Returns the otp_app that the Cluster was defined with.
      """
      def otp_app() do
        unquote(opts[:otp_app])
      end

      def get(path, params \\ [], headers \\ [], opts \\ []) do
        Request.request(__MODULE__, :get, path, nil, params, headers, opts)
      end

      def post(path, body \\ nil, params \\ [], headers \\ [], opts \\ []) do
        Request.request(__MODULE__, :post, path, body, params, headers, opts)
      end

      def put(path, body \\ nil, params \\ [], headers \\ [], opts \\ []) do
        Request.request(__MODULE__, :put, path, body, params, headers, opts)
      end

      def delete(path, params \\ [], headers \\ [], opts \\ []) do
        Request.request(__MODULE__, :delete, path, nil, params, headers, opts)
      end

      def patch(path, body \\ nil, params \\ [], headers \\ [], opts \\ []) do
        Request.request(__MODULE__, :patch, path, body, params, headers, opts)
      end

      def child_spec(opts) do
        %{
          id: __MODULE__,
          start: {__MODULE__, :start_link, [opts]},
          type: :supervisor
        }
      end

      def start_link(config \\ []) do
        otp_app = unquote(opts[:otp_app])
        config = Application.get_env(otp_app, __MODULE__, config)

        {:ok, config} = init(config)

        Supervisor.start_link(__MODULE__, otp_app, config)
      end
    end
  end

  @typedoc "The path of the HTTP endpoint"
  @type path :: String.t()

  @typedoc "The query params, which will be appended to the path"
  @type params :: Keyword.t()

  @typedoc "The body of the HTTP request"
  @type body :: String.t() | nil | binary() | map()

  @typedoc "Any additional HTTP headers sent with the request"
  @type headers :: Snap.HTTPClient.headers()

  @typedoc "Options passed through to the request"
  @type opts :: Keyword.t()

  @typedoc "The result from an HTTP operation"
  @type result :: success() | error()

  @typedoc "A successful results from an HTTP operation"
  @type success :: {:ok, map()}

  @typedoc "An error from an HTTP operation"
  @type error ::
          {:error, Snap.ResponseError.t() | Snap.HTTPClient.Error.t() | Jason.DecodeError.t()}

  @typedoc "Options available for configuring the Cluster"
  @type config_opts :: [
          url: String.t(),
          username: String.t(),
          password: String.t(),
          auth: Snap.Auth.t(),
          index_namespace: String.t() | nil,
          telemetry_prefix: list(atom()),
          http_client_adapter:
            Snap.HTTPClient.t() | {Snap.HTTPClient.t(), adapter_config :: Keyword.t()}
        ]

  @doc """
  Sends a GET request.

  Returns either:

  * `{:ok, response}` - where response is a map representing the parsed JSON response.
  * `{:error, error}` - where the error can be a struct of either:
    * `Snap.ResponseError`
    * `Snap.HTTPClient.Error`
    * `Jason.DecodeError`
  """
  @callback get(path, params, headers, opts) :: result()

  @doc """
  Sends a POST request.

  Returns either:

  * `{:ok, response}` - where response is a map representing the parsed JSON response.
  * `{:error, error}` - where the error can be a struct of either:
    * `Snap.ResponseError`
    * `Snap.HTTPClient.Error`
    * `Jason.DecodeError`
  """
  @callback post(path, body, params, headers, opts) :: result()

  @doc """
  Sends a PUT request.

  Returns either:

  * `{:ok, response}` - where response is a map representing the parsed JSON response.
  * `{:error, error}` - where the error can be a struct of either:
    * `Snap.ResponseError`
    * `Snap.HTTPClient.Error`
    * `Jason.DecodeError`
  """
  @callback put(path, body, params, headers, opts) :: result()

  @doc """
  Sends a DELETE request.

  Returns either:

  * `{:ok, response}` - where response is a map representing the parsed JSON response.
  * `{:error, error}` - where the error can be a struct of either:
    * `Snap.ResponseError`
    * `Snap.HTTPClient.Error`
    * `Jason.DecodeError`
  """
  @callback delete(path, params, headers, opts) :: result()

  @doc """
  Returns the config in use by this cluster.
  """
  @callback config() :: Keyword.t()

  @doc """
  Sets up the config for the cluster.

  Override this to dynamically load a config from somewhere other than your
  application config.
  """
  @callback init(Keyword.t() | nil) :: {:ok, Keyword.t()}
end