Skip to main content

lib/jido/chat/capability_matrix.ex

defmodule Jido.Chat.CapabilityMatrix do
  @moduledoc """
  Adapter capability declaration matrix (`:native | :fallback | :unsupported`).
  """

  alias Jido.Chat.Wire

  @statuses [:native, :fallback, :unsupported]

  @schema Zoi.struct(
            __MODULE__,
            %{
              adapter_name: Zoi.atom() |> Zoi.nullish(),
              capabilities: Zoi.map() |> Zoi.default(%{})
            },
            coerce: true
          )

  @type capability_status :: :native | :fallback | :unsupported
  @type t :: unquote(Zoi.type_spec(@schema))

  @enforce_keys Zoi.Struct.enforce_keys(@schema)
  defstruct Zoi.Struct.struct_fields(@schema)

  @doc "Returns the Zoi schema for CapabilityMatrix."
  def schema, do: @schema

  @doc "Creates normalized capability matrix payload."
  @spec new(map()) :: t()
  def new(attrs) when is_map(attrs) do
    capabilities =
      attrs[:capabilities] || attrs["capabilities"] || attrs[:matrix] || attrs["matrix"] || %{}

    attrs
    |> Map.put(:capabilities, normalize_capabilities(capabilities))
    |> Map.delete("capabilities")
    |> Map.delete(:matrix)
    |> Map.delete("matrix")
    |> then(&Jido.Chat.Schema.parse!(__MODULE__, @schema, &1))
  end

  @doc "Returns capability matrix map."
  @spec as_map(t()) :: %{optional(atom()) => capability_status()}
  def as_map(%__MODULE__{} = matrix), do: matrix.capabilities

  @doc "Returns declared support status for capability."
  @spec status(t(), atom()) :: capability_status()
  def status(%__MODULE__{} = matrix, capability) when is_atom(capability) do
    Map.get(matrix.capabilities, capability, :unsupported)
  end

  @doc "Serializes capability matrix into plain map with type marker."
  @spec to_map(t()) :: map()
  def to_map(%__MODULE__{} = matrix) do
    matrix
    |> Map.from_struct()
    |> Wire.to_plain()
    |> Map.put("__type__", "capability_matrix")
  end

  @doc "Builds capability matrix from serialized map."
  @spec from_map(map()) :: t()
  def from_map(map) when is_map(map), do: new(map)

  defp normalize_capabilities(capabilities) when is_map(capabilities) do
    capabilities
    |> Enum.map(fn {capability, status} ->
      {normalize_capability_key(capability), normalize_status(status)}
    end)
    |> Map.new()
  end

  defp normalize_capabilities(capabilities) when is_list(capabilities) do
    capabilities
    |> Enum.map(fn capability ->
      {normalize_capability_key(capability), :native}
    end)
    |> Map.new()
  end

  defp normalize_capabilities(_), do: %{}

  defp normalize_capability_key(capability) when is_atom(capability), do: capability

  defp normalize_capability_key(capability) when is_binary(capability) do
    capability
    |> String.trim()
    |> String.to_atom()
  end

  defp normalize_capability_key(capability), do: capability

  defp normalize_status(status) when status in @statuses, do: status

  defp normalize_status(status) when is_binary(status) do
    status
    |> String.trim()
    |> String.to_atom()
    |> normalize_status()
  rescue
    _ -> :unsupported
  end

  defp normalize_status(_), do: :unsupported
end