Why your Ruby class macros (might) suck (mine did)

Comments

An issue on GitHub just reminded me of a feature I added to Ransack a long while ago, but never got around to documenting. Sorry! :( Anyway, here's a quick writeup. I'll try and find time to get something written up for the official documentation as well, soon, if Ryan doesn't beat me to it. But this post is about more than how to limit searches in Ransack. It's about how I was stupid, and how I learned to be less stupid.

How it worked in MetaSearch

So, with MetaSearch, you could just use the attr_searchable and assoc_searchable class macros in order to define a list of attributes or associations that could be searched/traversed on a given base model. This was one of those things I did that seemed clever at the time, but was really pretty stupid of me. Sorry, again. :(

Why was it stupid to use a class macro?

It was stupid to use a class macro because Ruby already gives us all of the tools we need to exert much more powerful control over accessible attributes and associations: inheritance and super.

How it works in Ransack

ActiveRecord::Base has a default implementation of two methods: ransackable_attributes and ransackable_associations. They're really simple. Here they are:

def ransackable_attributes(auth_object = nil)
  column_names + _ransackers.keys
end

def ransackable_associations(auth_object = nil)
  reflect_on_all_associations.map {|a| a.name.to_s}
end

That's it! They return the string names of all columns and any defined ransackers, or the names of all associations, respectively.

You'll also note the acceptance of a single parameter, auth_object. When you call the search or ransack method on your model, you can provide a value for an :auth_object key in the options hash, which can be used in your own overridden methods. Putting this all together, you get the following example:

class Article
  def self.ransackable_attributes(auth_object = nil)
    if auth_object == 'admin'
      super
    else
      super & ['title', 'body']
    end
  end
end

> Article
=> Article(id: integer, person_id: integer, title: string, body: text) 
> Article.ransackable_attributes
=> ["title", "body"] 
> Article.ransackable_attributes('admin')
=> ["id", "person_id", "title", "body"] 
> Article.search(:id_eq => 1).result.to_sql
=> SELECT "articles".* FROM "articles"  # Note that search param was ignored!
> Article.search({:id_eq => 1}, :auth_object => 'admin').result.to_sql
=> SELECT "articles".* FROM "articles"  WHERE "articles"."id" = 1

That's it! Hopefully, the next time you're writing a class macro, you'll pause a moment to consider whether there's a better way. If not, well... hey, now you know how to whitelist/blacklist attributes in Ransack, at least!

comments powered by Disqus