Conversation
rsamoilov
left a comment
There was a problem hiding this comment.
Hi @anuj-pal27
That's a great start! I've left some comments. Please let me know if you have any questions.
| raise "Render was called multiple times in this action" if @__rendered | ||
| result = #{dynamic_method_name}(*args, **kwargs) | ||
| unless @__rendered | ||
| @__rendered = true | ||
| @__body << result.to_s | ||
| @__status = if status.is_a?(Symbol) | ||
| ::Rack::Utils::SYMBOL_TO_STATUS_CODE[status] || | ||
| raise(ArgumentError, "unknown status code: \#{status.inspect}") | ||
| else | ||
| status || 200 | ||
| end |
There was a problem hiding this comment.
This duplicates the logic from RageController::API#render, and it will critically complicate maintaining and extending the render method.
You should delegate the actual rendering to the render method itself - it already has the logic to store the body, convert the status, and set and validate the @__rendered flag.
The reason this is currently impossible is because render plain: ... always sets content type to text/plain; charset=utf-8. If you call render after calling the renderer proc, it will overwrite the content type that the user wanted to set in the proc. The solution I'd propose would be to ensure that render plain: ... only updates content type if it wasn't already set by the user. This way you can safely call render after calling the renderer proc.
There was a problem hiding this comment.
Got it! I'll delegate to render instead and fix render plain: to use ||= for content-type so it doesn't overwrite what the renderer block sets.
in lib/rage/controller/api.rb -> render method
@__headers["content-type"] ||= "text/plain; charset=utf-8"
RageController::API.class_eval <<~RUBY
def render_#{name}(*args, status: nil, **kwargs)
result = #{dynamic_method_name}(*args, **kwargs)
render plain: result, status: status
end
RUBYis this correct?
| @renderers = {} | ||
| end | ||
|
|
||
| attr_reader :renderers |
There was a problem hiding this comment.
I don't see this being used anywhere. Could you please add documentation to this method or remove it?
| def initialize | ||
| @renderers = {} | ||
| end |
There was a problem hiding this comment.
Let's move the initialization into the renderer method itself:
def renderer(name, &block)
@renderers ||= {}
# ...
end| if @renderers.key?(name) | ||
| raise ArgumentError, "a renderer named :#{name} is already registered" | ||
| end | ||
| dynamic_method_name = RageController::API.define_dynamic_method(block) |
There was a problem hiding this comment.
define_dynamic_method in RageController::API is obsolete - let's use the similar method from the Rage::Internal module instead.
There was a problem hiding this comment.
okk i will switch to Rage::Internal helper
dynamic_method_name = Rage::Internal.define_dynamic_method(RageController::API, block)
|
|
||
| Rage::Telemetry.__setup(@telemetry.handlers_map) if @telemetry | ||
|
|
||
| @renderers.each do |name, dynamic_method_name| |
There was a problem hiding this comment.
Could you please extract this loop into a separate method?
| # if the method exists but we didn't define it, someone else owns it — raise | ||
| if RageController::API.method_defined?(method_name) | ||
| raise ArgumentError, | ||
| "cannot register renderer :#{name} — `#{method_name}` is already defined" |
There was a problem hiding this comment.
This is great 👍
Let's also add the source location of the method to the error so that the user can see where exactly the method is already defined.
|
|
||
| # if this method was already defined by a previous finalize run, | ||
| # skip it to avoid false conflicts on app reload | ||
| existing = RageController::API.__custom_renderers[method_name] |
There was a problem hiding this comment.
I think it's the responsibility of the configuration class to track the applied custom renderers and ensure it doesn't reapply them. We shouldn't change RageController::API to implement this tracking.
One way to contain this logic inside the configuration class is to wrap renderer procs into a wrapper class inside the renderer method. The wrapper class would expose the proc itself and the applied? flag. Then, inside the finalize loop you would verify applied? is false and toggle it.
| # Tracks render_<name> methods defined by custom renderers so re-finalize | ||
| # can skip them instead of raising a false conflict error. | ||
| def __custom_renderers | ||
| @__custom_renderers ||= {} | ||
| end |
There was a problem hiding this comment.
See my suggestion about containing the tracking logic inside Configuration.
| # Tracks render_<name> methods defined by custom renderers so re-finalize | |
| # can skip them instead of raising a false conflict error. | |
| def __custom_renderers | |
| @__custom_renderers ||= {} | |
| end |
Summary
Implements the
config.renderer(:name) { ... }DSL proposed in the issue.Registers custom renderer blocks and generates
render_<name>methods onRageController::APIat boot time.Key behavior
config.renderer(:csv) { |obj, **opts| ... }registers a renderer block__finalize, every controller automatically getsrender_csv,render_phlex, etc.status:as both Symbol (:created) and Integer (201)ArgumentErrorinstead of setting nil statusheaders,request,paramsall workrendercallRageController::API.__custom_renderersregistry makes finalize idempotent across re-runsTests
9 specs covering:
renderinternallyNotes
Renderer blocks are converted to named methods via
define_dynamic_methodratherthan using
instance_execper request — this keeps the hot path allocation-freeand consistent with how Rage handles action methods internally.
Fixes: #234
Screenshots
CSV renderer:

Phlex renderer:
