lib/parser.ex

defmodule ExOpenAi.Parser do
  @moduledoc """
  Parses the response from the OpenAI API.
  """

  alias ExOpenAi.DeleteResponse

  @type metadata :: map
  @type http_status_code :: number
  @type key :: String.t()
  @type success :: {:ok, map}
  @type success_delete :: :ok
  @type error :: {:error, map, http_status_code} | {:error, :atom}

  @type parsed_response :: success | error

  @doc """
  Parse a response expected to contain a resource. If you pass in a
  module as the first argument, the JSON will be parsed into that module's
  `__struct__`.

  ## Examples
  Given you have a module named `Resource`, defined like this:
      defmodule Resource do
        defstruct sid: nil
      end

  You can parse JSON into that module's struct like so:
      iex> response = %{body: "{ \\"id\\": \\"cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7\\" }", status_code: 200}
      ...> ExOpenAi.Parser.parse(response, Resource, nil)
      {:ok, %Resource{id: "cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7"}}

  You can also parse into a regular map if you want.
      iex> response = %{body: "{ \\"id\\": \\"cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7\\" }", status_code: 200}
      ...> ExOpenAi.Parser.parse(response, %{}, nil)
      {:ok, %{"id" => "cmpl-uqkvlQyYK7bGYrRHQ0eXlWi7"}}
  """
  @spec parse({:ok, Tesla.Env.t()} | String.t(), module, boolean() | nil) :: success | error
  def parse(response, map, _simple)
      when is_map(map) and not :erlang.is_map_key(:__struct__, map) do
    handle_errors(response, fn body -> Jason.decode!(body) end)
  end

  def parse(response, module, simple) when is_binary(response) do
    handle_errors(response, fn body ->
      struct(module, Jason.decode!(body, keys: :atoms))
      |> module.keep_it_simple(simple)
    end)
  end

  def parse(response, ExOpenAi.Audio.Speech, _simple) do
    handle_errors(response, fn body ->
      %ExOpenAi.Audio.Speech{output: body}
    end)
  end

  def parse(response, module, simple) do
    handle_errors(response, fn body ->
      struct(module, Jason.decode!(body, keys: :atoms))
      |> module.keep_it_simple(simple)
    end)
  end

  @doc """
  Parse a list of resources.
  """
  @spec parse_list({:ok, Tesla.Env.t()} | String.t(), module, boolean() | nil) :: success | error
  def parse_list(response, module, simple) do
    handle_errors(response, fn body ->
      body
      |> Jason.decode!(keys: :atoms)
      |> case do
        %{object: "list", data: data} ->
          data
          |> Enum.map(fn item ->
            struct(module, item)
            |> module.keep_it_simple(simple)
          end)

        _ ->
          []
      end
    end)
  end

  @doc """
  Parse a deletion request.
  """
  @spec parse_delete({:ok, Tesla.Env.t()} | String.t()) :: success_delete | error
  def parse_delete(response) do
    handle_errors(response, fn body ->
      struct(DeleteResponse, Jason.decode!(body, keys: :atoms))
    end)
  end

  defp handle_errors(response, fun) when is_binary(response), do: {:ok, fun.(response)}

  defp handle_errors({:error, error}, _fun) do
    {:error, error}
  end

  defp handle_errors(response, fun) do
    case response do
      %{body: body, status: status} when status in [200, 201] ->
        {:ok, fun.(body)}

      %{body: _, status: status} when status in [202, 204] ->
        :ok

      %{body: body, status: status} ->
        {:error, Jason.decode!(body), status}
    end
  end
end