lib/ex_aws/operation/cloud_search.ex

defmodule ExAws.Operation.CloudSearch do
  @moduledoc """
  A datastructure representing an operation on AWS CloudSearch.

  This is fundamentally the same as `ExAws.Operation.JSON`, but it requires a
  separate `ExAws.Operation` implementation so that separate configurations do
  not have to be used for document API calls and search API calls. The
  `ExAws.Operation.CloudSearch` must be configured with the `domain`,
  and depending on the action being performed, one of the `config`, `doc`, or
  `search` domains will be filled into the `config` during execution.

  The `before_request` callback will be called before the host configuration is
  finalized or the CloudSearch API version is prepended to the path.

  Note: ExAws.CloudSearch only supports version 2013-01-01.
  """

  defstruct stream_builder: nil,
            service: :cloudsearch,
            http_method: :post,
            parser: nil,
            path: "/",
            data: %{},
            params: %{},
            headers: [],
            before_request: nil,
            api_version: "2013-01-01",
            request_type: :search

  @type t :: %__MODULE__{
          stream_builder: nil | (ExAws.Config.t() -> ExAws.Config.t()),
          service: :cloudsearch,
          http_method: :get | :post | :put | :patch | :head | :delete,
          parser: nil | fun(),
          path: String.t(),
          data: map,
          params: map,
          headers: list({String.t(), String.t()}),
          before_request: nil | (t, ExAws.Config.t() -> t),
          api_version: String.t(),
          request_type: :config | :doc | :search
        }

  def new(opts) do
    struct(%__MODULE__{parser: & &1}, opts)
  end
end

defimpl ExAws.Operation, for: ExAws.Operation.CloudSearch do
  @type response_t :: %{} | ExAws.Request.error_t() | no_return

  def perform(operation, config) do
    operation = handle_before(operation, config)
    {operation, config} = configure_host(operation, config)
    {operation, data, headers} = prepare_request(operation, config)

    url = ExAws.Request.Url.build(operation, config)

    headers = [{"accept", "application/json"}, {"x-amz-content-sha256", ""} | headers]

    operation.http_method
    |> ExAws.Request.request(url, data, headers, config, :cloudsearch)
    |> parse_response(config)
  end

  def stream!(_, _) do
    raise(ArgumentError, "This operation does not support streaming!")
  end

  defp handle_before(%{before_request: nil} = op, _) do
    op
  end

  defp handle_before(%{before_request: callback} = op, config) do
    callback.(op, config)
  end

  defp configure_host(
         %{api_version: version, request_type: :config} = operation,
         %{region: region} = config
       ) do
    verify_version(version)
    {operation, %{config | host: "cloudsearch.#{region}.amazonaws.com"}}
  end

  defp configure_host(
         %{api_version: version, path: path, request_type: type} = operation,
         %{region: region, search_domain: domain} = config
       ) do
    verify_version(version)

    {
      %{operation | path: "/#{version}/#{path}", service: :cloudsearch},
      %{config | host: "#{type}-#{domain}.#{region}.cloudsearch.amazonaws.com"}
    }
  end

  defp prepare_request(%{request_type: :search, http_method: :post} = op, config) do
    data =
      op
      |> parse_search_query(config)
      |> URI.encode_query()

    headers = [{"content-type", "application/x-www-form-urlencoded"} | op.headers]
    {Map.put(op, :params, %{}), data, headers}
  end

  defp prepare_request(%{request_type: :search} = op, config) do
    {Map.put(op, :params, parse_search_query(op, config)), %{}, op.headers}
  end

  defp prepare_request(%{data: data, headers: headers, request_type: :doc} = op, _config) do
    {op, data, [{"content-type", "application/json"} | headers]}
  end

  defp prepare_request(%{request_type: :config, http_method: :post} = op, _config) do
    data = URI.encode_query(op.params)
    headers = [{"content-type", "application/x-www-form-urlencoded"} | op.headers]
    {Map.put(op, :params, %{}), data, headers}
  end

  defp prepare_request(%{data: data, headers: headers} = op, _config) do
    {op, data, headers}
  end

  defp parse_response({:error, _} = result, _) do
    result
  end

  defp parse_response({:ok, %{body: ""}}, _) do
    {:ok, %{}}
  end

  defp parse_response({:ok, %{body: body}}, config) do
    {:ok, config[:json_codec].decode!(body)}
  end

  defp parse_search_query(%{params: %{} = params}, config) do
    {query, parser} =
      params
      |> Map.get_lazy("q", fn -> Map.fetch!(params, :q) end)
      |> ExAws.CloudSearch.QueryParser.parse()

    {fq, _} =
      params
      |> Map.get("fq", Map.get(params, :fq))
      |> ExAws.CloudSearch.QueryParser.parse()

    params
    |> put_param(:"q.parser", parser)
    |> put_param(:fq, fq)
    |> put_param(:q, query)
    |> Enum.reduce(%{}, &convert_json_params(config, &1, &2))
  end

  defp convert_json_params(config, {key, {:json, value}}, params) do
    Map.put(params, key, config[:json_codec].encode!(value))
  end

  defp convert_json_params(_config, {key, value}, params) do
    Map.put(params, key, value)
  end

  defp put_param(params, _, nil) do
    params
  end

  defp put_param(params, name, value) when is_atom(name) do
    params
    |> Map.put(to_string(name), value)
    |> Map.delete(name)
  end

  defp verify_version("2013-01-01") do
    nil
  end

  defp verify_version(version) do
    raise(ExAws.Error, "Unsupported CloudSearch API version #{version}")
  end
end