lib/ex_oapi/client/middleware.ex

defmodule Tesla.Middleware.RequestResponse do
  alias ExOAPI.Parser.V3.Context
  alias Context.{Schema, Operation}

  @moduledoc """
  """

  @behaviour Tesla.Middleware

  @impl Tesla.Middleware
  def call(env, next, opts) do
    env
    |> encode(opts)
    |> Tesla.run(next)
    |> case do
      {:ok, env} -> {:ok, decode(env, opts)}
      error -> error
    end
  end

  def encode(env, opts) do
    with ex_oapi_details <- Keyword.get(opts, :ex_oapi),
         {:ok, {body, content_type}} <- make_body_and_type(env, ex_oapi_details) do
      env
      |> Map.put(:body, body)
      |> Tesla.put_headers([{"content-type", content_type}])
    else
      # with multipart we let tesla handle the headers and content-type
      {:multipart, body} ->
        env
        |> Map.put(:body, body)

      :no_op ->
        env

      {:error, _} = error ->
        error
    end
  end

  def make_body_and_type(%{body: nil}, _), do: :no_op

  def make_body_and_type(_, %ExOAPI.Client{oapi_op: nil}),
    do: :no_op

  def make_body_and_type(
        %{body: body},
        %ExOAPI.Client{
          outgoing_format: outgoing_format,
          module: module,
          oapi_op: %Operation{
            servers: _servers,
            request_body: %Context.RequestBody{content: content}
          }
        }
      ) do
    case Map.get(content, outgoing_format) do
      %Context.Media{} = media -> {:ok, {outgoing_format, media}}
      nil when map_size(content) == 1 -> {:ok, hd(Map.to_list(content))}
      nil -> {:error, {:unexisting_media_format, outgoing_format}}
    end
    |> case do
      {:ok, {content_type, media}} ->
        encode_body(media, content_type, body, module)

      {:error, _} = error ->
        error
    end
  end

  def encode_body(_, content_type, body, _) when is_binary(body),
    do: {:ok, {body, content_type}}

  def encode_body(
        %Context.Media{
          schema: schema,
          encoding: encoding
        } = _media,
        content_type,
        body,
        module
      ) do
    spec_module = Module.concat(module, ExOAPI.Spec)
    %Context{} = spec = spec_module.spec()

    schema = get_schema(schema, spec)

    case encode_body_by_schema(schema, encoding, content_type, body, module) do
      {:ok, _} = ok -> ok
      {:error, _} = error -> error
      {:multipart, _} = multipart -> multipart
    end
  end

  def encode_body_by_schema(
        %Schema{},
        _encoding,
        "application/x-www-form-urlencoded" = content_type,
        body,
        _
      ),
      do: {:ok, {Plug.Conn.Query.encode(body), content_type}}

  def encode_body_by_schema(
        %Schema{},
        _encoding,
        "multipart/form-data",
        %Tesla.Multipart{} = body,
        _
      ),
      do: {:multipart, body}

  def encode_body_by_schema(
        %Schema{title: title},
        _encoding,
        "application/json" = content_type,
        body,
        module
      )
      when is_binary(title) do
    schema_module = Module.concat(module, title)

    if function_exported?(schema_module, :changeset, 2) do
      body
      |> schema_module.changeset()
      |> Ecto.Changeset.apply_action(:insert)
      |> case do
        {:ok, _} -> {:ok, {Jason.encode!(body), content_type}}
        {:error, changeset} -> {:error, changeset.errors}
      end
    else
      body
    end
  end

  @doc """
  Decode response body as querystring.

  It is used by `Tesla.Middleware.DecodeFormUrlencoded`.
  """
  def decode(env, opts) do
    env
    |> Map.update!(:body, &decode_body(&1, opts))
  end

  defp decode_body(body, opts), do: do_decode(body, opts)

  defp do_decode(data, _opts),
    do: Jason.decode!(data)

  defp get_schema(%Context.Schema{ref: nil} = schema, _), do: schema

  defp get_schema(%Context.Schema{ref: ref}, ctx),
    do: ExOAPI.Generator.Helpers.extract_ref(ref, ctx)
end