Posted by paul
Writing custom RSpec expectation matchers is a great way to enhance the readability of your specifications. As an example, here's a simple matcher to check whether a collection contains any of the given items. An example will make things clearer:
# Passing example
odds = [1,3,5,7,9]
evens = [2,4,6,8]
odds.should_not include_any(*evens)
One thing to note is that include_any expects multiple arguments rather than a collection, hence the splat. Here's the code:
module Spec
module Matchers
def include_any(*args)
IncludeAny.new(*args)
end
class IncludeAny
def initialize(*args)
@args = args
end
def matches?(target)
@target = target
@target.include_any?(*@args)
end
def failure_message
"expected #{@target.inspect} to include one or more elements from #{@args.inspect}"
end
def negative_failure_message
"expected #{@target.inspect} not to include any elements from #{@args.inspect}"
end
end
end
end
This is about as trivial as a custom matcher gets. All of the work is done by the include_any? method, which doesn't actually exist as part of the core Ruby API. In fact it's a simple extension to Enumerable that I think I originally borrowed from Merb. Put the following in config/initializers/core_extensions.rb or similar to use it in a Rails app.
module Enumerable
def include_any?(*args)
args.any? { |arg| self.include?(arg) }
end
end
Which custom matchers are you using?
Posted by paul
Mass assignment in Rails is super handy and saves heaps of code, especially in your controllers. As everyone knows however, hackers love mass assignment so you really need to use attr_accessible or attr_protected to protect your models.
But what happens if you want to mass assign model attributes which are otherwise protected? It would be simple enough to create your own method to do this, but if you take a look at the source you'll see that Rails already has it covered. Here's the method signature for ActiveRecord::Base#attributes=:
def attributes=(new_attributes, guard_protected_attributes = true)
That optional second parameter lets you bypass attr_accessible and attr_protected exactly as we'd like. But how do we call this? Well, you use send.
@model.send :attributes=, attributes, false
Posted by paul
Encouraged by a couple of kind comments I submitted a patch for the change I wrote about in my previous post, and as of yesterday you can now do something like this in edge Rails.
caches_page :index, :if => Proc.new { |c| !c.request.format.json? }
I'd not used Lighthouse before and I'm still getting my head around everything Git. If you're in a similar position, you might find these links useful too:
Posted by paul
Update 20/04/2008: I submitted a patch and this is now in edge Rails
When I created the JSON feed for my deadtre.es blog badge, one thing I didn't take care of was caching. As the contents of the feed will always be the same for a given URL no matter which user makes the request, it's a prime candidate for page caching. All I need to do is add caches_page :index to my controller, and Rails takes care of caching the index action for me. However, there's a problem here as the HTML format index action will also be cached, and unlike the JSON feed this does vary based on the logged on user (if you're looking at your own bookmarks you get edit and delete links for example) and so it isn't as suitable for page caching. What I'd really like to do here is to specify a condition that will be evaluated before the page is cached, much as the session method does.
caches_page :index, :if => Proc.new { |c| c.request.format.json? }
Unfortunately, a quick look at the docs for caches_page tells me that it doesn't support this. But looking at the source code I can see that caches_page is really just a nice shortcut for setting up an after_filter to perform the caching. So, instead of calling caches_page I could do this directly myself, and check the format of the page requested so that only the JSON feed is cached:
after_filter(:only => :index) { |c| c.cache_page if c.request.format.json? }
Now, although this works, it doesn't check that caching is enable (as caches_page does) and it's also not that pretty. Instead, I decided to override caches_page by dropping the following in my ApplicationController.
def self.caches_page(*actions)
return unless perform_caching
options = actions.extract_options!
after_filter(:only => actions) { |c| c.cache_page if options[:if].nil? or options[:if].call(c) }
end
This is pretty similar to the original caches_page except that it evaluates any :if proc passed in before caching the page. Now, I can now call caches_page with the syntax I originally wanted:
caches_page :index, :if => Proc.new { |c| c.request.format.json? }
Job done.
Posted by paul
When I wanted a quick (and arguably dirty) way to show my current reading list from deadtre.es on this blog, I figured that it was time to play with the JSON support which was baked in Rails 2.0. Wanting to keep things super simple, all I wanted to do was:
- Create a simple JSON feed I can include via a
<script> tag. (I'll write a simple Javascript callback which will insert my reading list into each page. If I decide to make this easier for users I'll put together the usual configuration page where people can play with the look and feel of their badge and get some code to copy and paste.)
- Keep things RESTful. Each user already has a URL for their bookmarks so I really wanted to avoid to avoid creating new controllers or actions.
Cutting to the chase, this pretty much turns out to be a one-liner in Rails. Here's an example:
class BookmarksController < ApplicationController
session :off, :only => :index, :if => Proc.new { |request| request.format.json? }
def index
# fetch bookmarks ...
respond_to do |format|
format.json do
excluded_book_attrs = [:aws_domain, :id, :created_at, :updated_at]
excluded_bookmark_attrs = [:book_id, :id, :updated_at, :user_id]
render :json => @bookmarks.to_json(:except => excluded_bookmark_attrs, :include => { :book => { :except => excluded_book_attrs }}), :callback => 'show_deadtrees_books'
end
end
end
end
It doesn't get much simpler than that! There are a couple of things worth pointing out:
render :json takes care of setting the content type to application/json in the reponse header. Additionally, it wraps the JSON in a call to the method specified by the :callback option. (I should allow users to override this via the querystring to avoid name conflicts in their Javascript or to remove the callback altogether to support use outside of a <script> tag.)
- You can include Active Record associations when calling
to_json using :include
- You can also control which attributes are serialized (including those on associations) using the
:only and :except options.
- I've disabled sessions for JSON requests to the index action since they're not required and carry a reasonable overhead.
With that in place, all you need to do is implement your Javascript callback (borrow mine if you like) and your blog badge is done!
Posted by paul
I suppose the laziest way to describe deadtre.es is as 'del.icio.us for books'. I've been tinkering with it on and off over the last couple of months or so, and thought it time I polished off the roughest edges and gave it a mini-launch here. As far as functionality goes it's missing some fairly obvious stuff at the minute, but it's usable and includes a bookmarklet for adding books from the pages of Amazon and a simple JSON feed should you want to display your reading list on your blog. If you want something a little more comprehensive you might want to check out Library Thing, BookJetty or maybe Amazon Wish Lists, but if you want a super simple way to keep and share a reading list you're more than welcome to give deadtre.es a go.
Posted by paul
Getting the acts_as_taggable_on_steroids and will_paginate plugins playing nicely together has been written about plenty of times before, but I still ended up doing my own thing when I tackled this on a project of mine. This topic came up in conversation again recently so I guess it's time I wrote up my solution.
Using the typical blog application as an example you might have a Post model which acts_as_taggable. Together, the plugins give you a Post.paginate_tagged_with method which looks as though it will do what you want, but unfortunately doesn't work as expected. You may well get the correct results, but the SQL that is generated to count the total number of results is incorrect so you'll see some strange behavior in you paging controls.
The solution I settled on, was to create a custom finder (paginater?) on the Post model. It looks like this:
class Post < ActiveRecord::Base
acts_as_taggable
def self.paged_find_tagged_with(tags, args = {})
if tags.blank?
paginate args
else
options = find_options_for_find_tagged_with(tags, :match_all => true)
options.merge!(args)
# The default count query generated by paginate includes COUNT(DISTINCT Posts.*) which errors, at least on mysql
# Below we override the default select statement used to perform the count so that it becomes COUNT(DISTINCT Posts.id)
paginate(options.merge(:count => { :select => options[:select].gsub('*', 'id') }))
end
end
end
You use it as you would the regular paginate_tagged_with:
@posts = Post.paged_find_tagged_with('rails', :page => 1)
The way this is works is that we first call find_options_for_find_tagged_with to get a hash of options required to perform a find by tag. If we hand this straight to paginate we'll get an error trying to SELECT COUNT(DISTINCT Posts.*). Thankfully however, will_paginate lets you override the options it uses to generate its count SQL. If we take options[:select] and replace the '*' with 'id' and use that as the :select for our :count, the SQL generated is SELECT COUNT(DISTINCT Posts.id) and everything works as expected.
That's it, really. It's worth pointing out that when no tags are passed in I'm calling paginate (without any additional options) to return all posts. This makes sense in my app and moves some logic out of the controller but you might want to do something different.