Skip to content

[Rendering] Custom renderer support #234

@rsamoilov

Description

@rsamoilov

Description

Out of the box, Rage supports rendering JSON, plaintext, and SSE. However, many applications need to render other formats — HTML via Phlex or ERB, CSV, XML, Protocol Buffers, etc. Today, this is possible but requires manually setting the content type:

render plain: MyPhlexComponent.call
headers["content-type"] = "text/html"

This works, but it's not ergonomic for frequent use, and it doesn't provide a consistent pattern across an application. The goal of this issue is to introduce a standard way to register custom renderers, making it straightforward to plug in view-level gems and enable server-side rendering workflows. This work will also serve as the foundation for the Inertia.js integration.

Proposed interface

Register renderers in configuration:

# config/application.rb or config/environments/<environment>.rb
Rage.configure do
  config.renderer(:phlex) do |object, **options|
    headers["content-type"] = "text/html"
    object.new(**options).call
  end

  config.renderer(:csv) do |object, delimiter: ","|
    headers["content-type"] = "text/csv"
    CSV.generate(object, col_sep: delimiter)
  end
end

Use in controllers:

class ArticlesController < RageController::API
  def index
    render_phlex MyComponent, articles: Article.all
  end

  def export
    render_csv my_csv_data, delimiter: ";"
  end
end

Registering a renderer named :phlex would define a render_phlex method available in all controllers. The proc's return value becomes the response body.

Design considerations

These are open questions that should be addressed in the design proposal:

  • Execution context. The proc references headers directly, which means it needs to execute in the controller's context. This gives the proc access to request, params, cookies, etc.
  • Return value semantics. In the proposed interface the proc's return value becomes the response body as a string. Consider whether a more flexible approach would be to explicitly use render instead. This would allow to support streaming bodies and custom status codes, but could be more verbose.
  • Name conflicts. What happens if someone registers duplicate renderers?
  • Method definition timing. Since renderers are registered at config time and define controller methods, ensure the methods are available after code loading. Consider how this interacts with Rage's code reloading in development.

Before You Start

This issue requires a design proposal before implementation. Please open a discussion or comment on this issue with a high-level plan that outlines:

  1. The public API (confirming or adjusting the interface above).
  2. How renderer procs will be stored and how controller methods will be defined.
  3. The execution context strategy (instance_exec or alternative).
  4. How the edge cases above will be handled.

PRs without prior design discussion are unlikely to be accepted.

Tips

  • Look at how render json: and render plain: are currently implemented in the codebase to understand the rendering pipeline and where custom renderers would hook in.
  • Check the architecture doc that shows how Rage's core components interact with each other and outlines the design principles.
  • For context on the Inertia.js use case, see inertia-rails to understand what the rendering integration needs to support.
  • Feel free to ask any questions or request help in the comments below!

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions