Skip to main content

lib/anantha_json/json_adapter.ex

defmodule AnanthaJson.JsonAdapter do
  @moduledoc """
  Defensive JSON decoder that tries `JsonRemedy.repair/1` first for
  malformed LLM output, falling back to `Jason.decode/2`.

  This decouples the pipeline from the `json_remedy` dependency. If
  `json_remedy` is unavailable (e.g. not compiled), we silently fall
  back to standard JSON decoding.
  """

  @doc """
  Decodes a JSON string, attempting repair first.

  Returns `{:ok, decoded}` on success or `{:error, reason}` on failure.
  """
  @spec decode(String.t()) :: {:ok, term()} | {:error, term()}
  def decode(json_string) when is_binary(json_string) do
    case try_json_remedy(json_string) do
      {:ok, decoded} when decoded != "" ->
        {:ok, decoded}

      :fallback ->
        Jason.decode(json_string)

      {:ok, _} ->
        Jason.decode(json_string)

      {:error, _reason} ->
        Jason.decode(json_string)
    end
  end

  def decode(_), do: {:error, :invalid_input}

  @doc """
  Like `decode/1` but raises on error.
  """
  @spec decode!(String.t()) :: term()
  def decode!(json_string) when is_binary(json_string) do
    case decode(json_string) do
      {:ok, decoded} -> decoded
      {:error, reason} -> raise "JsonAdapter failed: #{inspect(reason)}"
    end
  end

  defp try_json_remedy(json_string) do
    if Code.ensure_loaded?(JsonRemedy) && function_exported?(JsonRemedy, :repair, 1) do
      case JsonRemedy.repair(json_string) do
        {:ok, result, _repairs} -> {:ok, result}
        {:ok, result} -> {:ok, result}
        {:error, _reason} = error -> error
      end
    else
      :fallback
    end
  end
end