Writing a Rails Metal app can make you realize just how spoiled we’ve become with all the convenience that comes with Rails. Without the controller and view helpers, it can become a painful experience. Here’s a guide to help make it a better experience.
For this guide, we’re writing a Widget Refresher Metal app. Supposedly, the widget page of our Rails application gets called too much, and so we want to take advantage of Metal. Under our project’s app/metal folder, we create refresher.rb:
class Refresher < Rails::Rack::Metal
def self.call(env)
refresher = RefresherApp.new
refresher.call(env)
end
end
class RefresherApp
def call(env)
# refresh widget path: /widgets/:id/refresh
if env["PATH_INFO"] =~ /^\/widgets\/(\d+)\/refresh/
widget_id = $1
prepare(env, widget_id)
refresh
else
[404, { "Content-Type" => "text/html" }, "Not Found"]
end
end
# to setup the environment
def prepare(env, widget_id)
...
end
# the heart of our Metal app
def refresh
...
end
end
I like to create a separate class RefresherApp instead of just writing all of it inside the Refresher class (the one that extends from Rails::Rack::Metal). When your Metal app becomes more than just a trivial hello world app, you’ll be needing a bunch of methods calling each other. Since the call method in the Metal app is a class method, putting all the code in one class will require all these methods to be class methods as well. And I think that looks ugly. Feel free to stick it all in one class if you want. If you do, you can change the context to self so you don’t have to keep on defining each method as self.method:
class Refresher < Rails::Rack::Metal
# the methods in here are class methods
class << self
def call(env)
...
end
def method
...
end
end
end
For the rest of the guide, we’re using my approach. Also, note that when developing a Metal app, you need to keep on restarting your server for your code changes to take effect.
Request and Session
To access the request and the parameters in it, you can use this code:
request = Rack::Request.new(env) params = request.params params['mykey'] # String keys, so not params[:mykey]
As you can see, the keys will be of class String, not Symbol. Now for the session, you can get it from the environment:
session = env['rack.session']
We can move all these code into our prepare method. In addition, we can set the params[:id] (using a Symbol if you want), so that in our main refresh method, it would be like in a Rails controller. With the session, we can get the current user. We can also define other methods to make things more like writing code for a Rails controller. This is how it looks like:
attr_reader :request, :session, :current_user def params @request.params end def logged_in? !!current_user end def prepare(env, widget_id) @request = Rack::Request.new(env) params[:id] = widget_id @session = env["rack.session"] @current_user = session[:user_id] ? User.find(session[:user_id]) : false end
With these out of the way, we go into writing the code for the main method called refresh.
refresh and ActiveRecord
ActiveRecord works out of the box, no setup needed. Cool! Let’s say we just need to return the status of widget to the client side:
def refresh
@widget = Widget.find(params[:id])
return [200, { "Content-Type" => "text/html" }, @widget.status]
end
We can also send javascript code, or other content types back to our client. Just make sure to set your content type properly. Let’s also add some simple checking if the user is logged in:
def refresh
@widget = Widget.find(params[:id])
if logged_in?
return [200, { "Content-Type" => "text/javascript" }, "Element.update('status', '#{@widget.status}');"]
else
return [200, { "Content-Type" => "text/javascript" }, "Element.update('message', 'Must be logged in for widget status to refresh');"]
end
end
When returning more complex javascript however, it’s probably better to escape the newlines and the quotes or we’ll get parsing errors on the browser side. Rails provides a helper method called escape_javascript, but a Metal app doesn’t have access to helpers by default. So…
View Helpers
To use helpers in your Metal app, just include the modules you need:
include ActionView::Helpers::JavascriptHelper # so escape_javascript works include WidgetsHelper # for example
I prefer to avoid including too much of these helpers though.
Request Forgery Protection
If the request is not a GET request, we may need to verify the authenticity token. Here’s one way to do it:
def refresh
# before everything else
return redirect_to_widgets_response unless verified_request?
# everything else
...
end
def redirect_to_widgets_response
return [302, { "Content-Type" => "text/html", "Location" => "/widgets" },
"<html><body><a href=\"/widgets\">Redirecting...</a></body></html>"]
end
# Based on Rails method of the same name but simplified, i.e. no need to check if:
# - protection is disabled
# - request method is :post
# - format is verifiable
def verified_request?
form_authenticity_token == params['authenticity_token']
end
def form_authenticity_token
session[:_csrf_token] ||= ActiveSupport::SecureRandom.base64(32)
end
More Challenges
There are other challenges you may encounter in writing your Rails Metal app. I have tried rendering a partial by directly using ERB but it’s too ugly to show here. And I’ve also struggled with performance. Not all business logic can simply be translated to a Metal app to be fast. It is recommended for very simple things only, or it may not be worth it. Anyway, I hope this guide clears up a few things. Also, if you have better ways of doing any of the above, feel free to post in the comments. Thanks!
Thanks so much. I have been looking for a way to use params and session for a few days.
Hello,
I’m trying this:
class Poller
def self.call(env)
if env[”PATH_INFO”] =~ /^\/poller/
@posts = Post.find(1, :select => “title”)
[200, {”Content-Type” => “text/html”}, @posts.title]
else
[404, {”Content-Type” => “text/html”}, [”Not Found”]]
end
end
end
and the performace is very sux…
what’s the problem?
@Michael: You’re welcome.
@Diego:
Can you provide any numbers on that so we get an idea how much it sucks? So you compared this to a non-Metal controller doing about the same thing and that was faster (Metal version is slower)?
Also, make sure you are in production mode when testing for performance. If you’re already in production mode, I can only guess that the DB call is slow. Performance of queries to DB can change, so you might want to get more samples for your benchmark, or mock the Post and stub the find method so that line won’t touch the DB (only for your perf test of course).
[…] Guide to Rails Metal - Someone has actually been messing around with this enough to write about it. […]
If you’re fetching Active Records and rendering templates and partials, you’ve probably ventured outside the area where metal is going to give you a worthwhile boost. Have you benchmarked this vs a simple Action Controller? I doubt you’re going to see a big difference.
I’d reserve Metal use to something even simpler. Like outputting prerendered text from a memcached server after doing a simple check for permissions (not going through Active Record).
@DHH: Yeah, you’re right. I learned that the hard way. I had to scrap my metal approach for the partials, but thought I might as well post the things I’ve learned about accessing request params, sessions, etc.
[…] Michael Galero posted a helpful guide to understand how to write such an app. This entry was written by admin, posted on February 10, 2009 at 5:00 am, filed under Ruby on Rails. Bookmark the permalink. Follow any comments here with the RSS feed for this post. Comments are closed, but you can leave a trackback: Trackback URL. « 10 AMAZING FREE STOCK PHOTOGRAPHY SITES […]
[…] michael galero » Blog Archive » Guide to Rails Metal (tags: ruby rails reference) Possibly related posts: (automatically generated)links for 2006-12-12RadRails IDE for Ruby on RailsRuby on Rails CritiqueCool new Ruby on Rails startups […]
[…] michael galero » Blog Archive » Guide to Rails Metal devblog.michaelgalero.com/2009/02/03/guide-to-rails-metal – view page – cached Writing a Rails Metal app can make you realize just how spoiled we’ve become with all the convenience that comes with Rails. Without the controller and view helpers, it can become a painful experience. Here’s a guide to help make it a better experience. — From the page […]