ActsAsRenderer Brings Output to Models

Judging by the fact that there are several posts about this topic out in the wild, and that I have come across a need for it more than once, I thought it would be helpful to wrap up this functionality into a plugin and put it out into the world. Give a warm welcome to ActsAsRenderer!

Before you go off on a tirade about the evils of violating MVC, let me first say I know the arguments and I agree with you. However, in a world of complex systems where not everything is done via full-stack HTTP, there are legitimate reasons to output data directly from models, and ActsAsRenderer helps you do it.

With ActsAsRenderer, you get four cool new functions.

For your model class, you get render_file and render_string. For your instances, you get render_to_file and render_to_string.

Probably the most common (and legitimate) use of this kind of functionality is for rendering data out of a Rails script (say with script/runner). Since that environment is not a full-stack HTTP view of the world, it’s a real pain to render any kind of structured output. Not anymore! With acts_as_renderer in your model, you can render your views and give your model the voice it’s been lacking!

I’ve had this need come up several times. Most recently, I built a server configuration management system using Rails. While it is nice to preview the rendered configuration files using Rails-over-HTTP, it is also essential to be able to write those same configuration files out to the filesystem. In another case, I had a background DRb process that needed to be able to render templated output to the filesystem. I had to go build a mock-controller and do some pretty unsavory things; all of that would have been obviated with acts_as_renderer.

Now, I can simply say:

class Server < ActiveRecord::Base  acts_as_renderer

  def build_configuration    CLIENT_CONFIG_FILES.each do |f|      render_to_file("configs/#{f}", "#{config_dir}/#{f}.conf")    end  endend

The render_to_file function renders the templates located in configs (under app/views by default) and writes them to the files specified in the config_dir; it’s also smart enough to know that render_to_file is being called from a ‘server’ instance and sets @server accordingly. So my templates in configs are simply:

; Configuration Snippet for Server <%=@server.description%>

<%= render :partial => 'configs/queue', :collection => @server.queues %>

Please do think before using this plugin. It can be used for some seriously evil violations of good MVC design practice, and you are responsible for your own actions. However, this can also be used to make your existing designs *much* more robust and elegant, and I encourage you to use it where that is true.

It’s ready to drop in. Everything is there, including tests. Enjoy!

NOTE: Version 1.0 only supported Rails 2.0; I just added version 1.01 which will work with either Rails 1.2.x or 2.0.x. Please feel free to ping me with any questions.

acts_as_renderer at RubyForge

8 comments ↓

#1 dasil003 on 03.10.08 at 7:55 pm

What, no comments? This plugin is awesome!

Glad you made the MVC comment, because it’s easy to dismiss the need for this. But for those of us who know what we’re doing this is indispensable.

Rails controllers and views are made primarily for rendering to a browser, but HTML and other output often times needs to go other places. The alternatives to this plugin are HTML in models or rolling your own template system, either way pretty ugly.

Anyway, one comment on this plugin. We just installed it into our current project and had one problem. We are using some constants in our controllers, and since plugins are loaded before initializers, we get LoadErrors. Our solution is to put the plugin in lib/ and load it manually. Any ideas for a cleaner solution?

#2 Dave Troy on 03.11.08 at 10:07 pm

This is an interesting problem. Not sure exactly what you’re trying to accomplish in practice, but perhaps the constants could be put into environment.rb?

The acts_as_renderer plugin only subclasses ActionController:Base and should not need anything too peculiar. If you can tell me a bit more about specifically what you think you’re adding that’s breaking this (and where you’re adding it) I might be able to come up with a tidy fix.

I’ve used this to great ends this week! I know I will be using this plugin myself for several projects in the future.

#3 tigger on 03.13.08 at 8:41 am

Thanks for taking the time to do this cleanly, it’s just what we’ve been trying to do.

I’ve a small issue with using it though, which is that the application_helper doesn’t seem to have been loaded and I can’t quite figure out how to get that working.

Any ideas?

#4 Rob Holland on 03.14.08 at 6:01 am

Changing the self.render_string function to that listed below makes sure that the application helper is also loaded:

viewer = Class.new(ApplicationController)
view = Class.new(ActionView::Base)
view.send(:include, viewer.master_helper_module)
path = ActionController::Base.view_paths rescue ActionController::Base.view_root
view.new(path, assigns, viewer).render(template)

#5 Dave Troy on 03.14.08 at 11:37 am

Rob, your fix is exactly what I would suggest. I debated whether to base the viewer on ActionController::Base or ApplicationController, and the helper support is the obvious reason to go for the latter. I was trying to keep it as simple as possible.

I will put that into the next revision. Meantime you can just make the change as suggested. Thanks for the feedback everyone.

#6 walt D on 04.13.08 at 7:58 am

Hi Dave,

I’m trying to figure whether this plugin will help me do this:

I have an Invoices controller which renders a “show.html.erb” template (without layouts) – and I’d like this to go into a file instead of being presented to the user.

Right now I call a system command which curl -G http://localhost:3000/invoices/1 -o invoice_1.html – but my provider will not let me do this ๐Ÿ™

Can I use this plugin – or do you know of some other way?

best regards,
Walther

#7 walt D on 04.13.08 at 8:41 am

Well – so I tried using your plugin – and failed miserably at doing so ๐Ÿ™

I’m sure I’m to blame, myself – but perhaps you can point the finger more precisely at me ๐Ÿ™‚

I added acts_as_renderer to my class Invoice < ActiveRecord::Base and threw a def build_invoice in there too.

When I tried using it – something in your plugin/init.rb blows up – and the result is that I get an error page when I try accessing any form new or form/edit in my app, with a note telling me that a copy of an ApplicationHelper was removed but still active.

I have a class ExtendedLayoutFormBuilder < ActionView::Helpers::FormBuilder and that apparently gets hurt pretty bad by your plugin ๐Ÿ™ I’m not sure what your plugin actually do, but somehow it breaks my extendedformbuilder As I set out – I’m sure that I’m to blame, but all my layoutbuilder do is:
helpers = field_helpers +
%w(date_select datetime_select time_select collection_select) –
%w(label fields_for)

helpers.each do |name|
define_method name do |field, *args|
options = args.detect {|argument| argument.is_a?(Hash)} || {}
build_shell(field, options) do
super
end
end
end

def build_shell(field, options)
@template.capture do
locals = {
:element => yield,
:label => label(field, (options[:label] || “”) ),
:lbl => options[:label],
:frmless => options[:frmless],# || nil,
:required => options[:required],# || nil,
:css => options[:class],
:instruction => options[:instruction]# || nil
}
if has_errors_on?(field)
locals.merge!(:error => error_message(field, options))
@template.render :partial => ‘forms/field_with_errors’,
:locals => locals
else
@template.render :partial => ‘forms/field’,
:locals => locals
end
end
end

def error_message(field, options)
if has_errors_on?(field)
errors = object.errors.on(field)
errors.is_a?(Array) ? errors.to_sentence : errors
else

end
end

def has_errors_on?(field)
!(object.nil? || object.errors.on(field).blank?)
end

#8 Mr. Weir on 11.21.08 at 2:04 am

Thank you very much for creating this. It is a wonderful solution.