Skip to main content

lib/pi/output/renderable.ex

defprotocol Pi.Output.Renderable do
  @moduledoc "Converts eval-domain values into pi structured output when possible."

  @fallback_to_any true

  @doc "Returns a `Pi.Output` for `value`, or `nil` when the value has no semantic renderer."
  @spec to_output(term(), keyword()) :: Pi.Output.t() | nil
  def to_output(value, opts)
end

defimpl Pi.Output.Renderable, for: Pi.Output do
  def to_output(output, _opts), do: output
end

defimpl Pi.Output.Renderable, for: List do
  def to_output(value, opts), do: Pi.Output.list_output(value, opts)
end

defimpl Pi.Output.Renderable, for: Map do
  def to_output(value, opts), do: Pi.Output.map_output(value, opts)
end

defimpl Pi.Output.Renderable, for: BitString do
  def to_output(value, opts), do: Pi.Output.text(value, opts)
end

defimpl Pi.Output.Renderable, for: Pi.Docs.Result do
  def to_output(result, opts) do
    rows =
      Enum.map(result.entries, fn entry ->
        %{
          module: inspect(entry.module),
          kind: to_string(entry.kind),
          name: to_string(entry.name),
          arity: entry.arity,
          summary: entry.summary,
          source: entry.source,
          line: entry.line
        }
      end)

    Pi.Output.table(
      rows,
      Keyword.put_new(opts, :columns, [:module, :kind, :name, :arity, :summary, :line])
    )
  end
end

defimpl Pi.Output.Renderable, for: Pi.Docs.Entry do
  def to_output(entry, opts) do
    entry
    |> Map.from_struct()
    |> Map.update!(:module, &inspect/1)
    |> Pi.Output.tree(opts)
  end
end

defimpl Pi.Output.Renderable, for: Pi.Web.Result do
  def to_output(result, opts) do
    preview = Keyword.get(opts, :preview, preview(result))

    part =
      Pi.Protocol.Tool.OutputPart.document(result.text,
        language: language(result.format),
        title: preview,
        data: metadata(result)
      )

    %Pi.Output{parts: [part], text: result.text}
  end

  defp preview(result) do
    title = if result.title in [nil, ""], do: result.final_url || result.url, else: result.title
    status = result.status || "?"
    suffix = if result.truncated?, do: " · truncated", else: ""
    "Web fetch · #{status} · #{title}#{suffix}"
  end

  defp language(:json), do: :json
  defp language(:html), do: :html
  defp language(:markdown), do: :markdown
  defp language(_format), do: :text

  defp metadata(result) do
    %{
      document_kind: :web_fetch,
      url: result.url,
      final_url: result.final_url,
      status: result.status,
      content_type: result.content_type,
      format: result.format,
      title: result.title,
      size_bytes: result.size_bytes,
      total_chars: result.total_chars,
      truncated: result.truncated?,
      redirected: result.redirected?
    }
  end
end

defimpl Pi.Output.Renderable, for: Pi.Docs.Source do
  def to_output(source, opts) do
    Pi.Output.code(source.text, Keyword.get(opts, :language, :elixir),
      title: Keyword.get(opts, :title) || Keyword.get(opts, :preview, source.subject),
      data: %{
        source_path: source.source,
        start_line: source.start_line,
        end_line: source.end_line,
        subject: source.subject
      }
    )
  end
end

defimpl Pi.Output.Renderable, for: Any do
  def to_output(_value, _opts), do: nil
end