How to share code among ActiveRecord models

My last project was migrating a Rails 2.3 app to Rails 3.2. At some point I was updating tens of definitions of named scopes, a pretty easy and tedious task.

When I see many similar code snippets, generalizations popup in my mind. That never fails, and this time I found many named scopes that could be parametrized in some way or another. So I did it and started replacing those occurrences with calls to my generic scopes.

Later on, when it came time to test whether my changes worked or not… (f*ck) I found out there was a pretty good reason why a bunch of them were the way they were, i.e. not parametrized. Aargh

The first generic scope: or_where

To explain the issue I need to take a little detour. One scope I could not live without anymore was the where with an OR, i.e. the where that makes the OR of its arguments. It’s very strange that Rails implements only the where with an AND. I mean, anyone knows that AND and OR go hand in hand…

module GenericScopes

  module ClassMethods

    def merge_conditions_with_or(*conditions)
      segments = []
      conditions.each do |condition|
        unless condition.blank?
          sql = sanitize_sql(condition)
          segments << sql unless sql.blank?
        end
      end
      "(#{segments.join(') or (')})" unless segments.empty?
    end

  end



  def self.included(model)

    model.extend ClassMethods

    model.class_eval do

      model.named_scope :or_where, lambda { |*conditions|
        split_conditions = []
        conditions.each { |condition|
          if condition.is_a?(Hash)
            condition.each { |key, value|
              split_conditions << {key => value}
            }
          else
            split_conditions << condition
          end
        }
        sql = merge_conditions_with_or(*split_conditions)
        where(sql)
      }

    end

  end

end

About the GenericScopes module, the interesting thing to note is that it will work without modifications with the final solution I’m going to describe here. In fact, its code could be buggy (I didn’t even try to make it work as nicely as the default where) but it works pretty fine for my own needs. (See an example at the end of this post.)

The problem with the default where is that the AND of the arguments is hardcoded into the merge_conditions method. Instead of changing anything into the ActiveRecord code, I opted for copy-and-pasting that method and replacing the AND with an OR. I use lower case SQL so that when I inspect queries I can easily understand if a piece of query was generated by me or by Rails.

The issue I’m trying to explain pops up with the call to sanitize_sql from merge_conditions_with_or. If the method was public it could work directly like this

User.sanitize_sql(:full_name => 'John Doe')
  # => ["`users`.`full_name` = 'John Doe'"]

but, if an exception wasn’t raised, it would be like this

User.or_where(:full_name => 'John Doe') 
  # internally, the sanitize_sql would return 
  # => ["``.`full_name` = 'John Doe'"]

In fact, when the call is applied to User, and when it takes place inside of merge_conditions_with_or (even if called from or_where which is in turn applied to User), the values of self are completely different. While in the former case it’s User (concrete), in the latter self is ActiveRecord::Base (abstract).

Problem

The problem was the way generic scopes were made available to models.

ActiveRecord::Base.send(:include, GenericScopes)

The inclusion of GenericScopes into ActiveRecord::Base was the cause of all troubles with sanitize_sql. It had been working fine for years in production, but for my new scopes I needed to move the inclusion of the module from the abstract ActiveRecord::Base to each and every concrete model, like this:

class User
  include GenericScopes
  #...
end

class Post
  include GenericScopes
  #...
end

#...

Solution

That is quite boring and you must remember to include GenericScopes in all models. Luckily Ruby provides the inherited method. The following code is then a pretty good way to share code among ActiveRecord models. The nice things are that (1) it follows the DRY principle, (2) it is completely automatic, (3) shared code is properly bound to the inheriting class.

module ActiveRecord
  class Base
    class << self
      def inherited_with_generic_scopes(model)
        inherited_without_generic_scopes(model)
        model.send(:include, GenericScopes)
      end
      alias_method_chain :inherited, :generic_scopes
    end
  end
end

being or being_not, that is the question

It’s very common to have many boolean columns in the same table, so it’s useful to have a generic scope for them. Using where and or_where, these are all the combinations:

model.named_scope :being,        lambda { |*columns|    where(columns.map{|x| {:"#{x}" => true}}.reduce(&:merge)) }
model.named_scope :or_being,     lambda { |*columns| or_where(columns.map{|x| {:"#{x}" => true}}.reduce(&:merge)) }
model.named_scope :being_not,    lambda { |*columns|    where(columns.map{|x| {:"#{x}" => false}}.reduce(&:merge)) }
model.named_scope :or_being_not, lambda { |*columns| or_where(columns.map{|x| {:"#{x}" => false}}.reduce(&:merge)) }

And here is an example about how to use them:

Song.or_being('reggae', 'ska', 'ska_punk')

How to highlight Ruby code on the web

I’m using my own Chili plugin on this website, and I made it exactly for that: highlighting my snippets on my websites. It is very good for many languages, but not for Ruby. Some years ago I started developing a new version, much more powerful than ever, but later I was sucked up into life again and I left that behind…

So now that I wanted to publish some Ruby code here, I recently wandered in the internet looking for some kind of replacement, or at least something that I could use side by side with Chili, and which could highlight Ruby. I mean, highlight Ruby as real Ruby, because I could easily write any fake Ruby recipe for Chili myself… But if I had this kind of code I want it highlighted as it should:

{[ .snippet | 1.hljs(=ruby=) ]}

Note that such a snippet is perfectly valid Ruby code (results in a == “#}1”) but it is easy to understand why a highlighter could fail on it. And many do.

Luckily I eventually stumbled upon highlight.js (by Ivan Sagalaev) which is excellent for Ruby (and I think all other languages it supports, too). As you see above, it correctly understands how string interpolation works in Ruby. That is thanks to its context based architecture (which Chili lacks).

Use this CSS to reset PRE tags:

{[ .reset-pre | 1.hilite(=css=) ]}

Use this HTML to setup highlight.js for coloring PRE blocks containing CODE blocks with a class “hljs”. As usual Chili will ignore blocks with languages it doesn’t have a recipe for, and highlight.js will instead consider only its own blocks.

{[ .js-config | 1.hilite(=html=) ]}

 

Rails 4 (beta1) depends on Rdoc ~>3.4 but Ruby 2 on Rdoc 4

Gemfile.lock

...
railties (4.0.0.beta1)
  actionpack (= 4.0.0.beta1)
  activesupport (= 4.0.0.beta1)
  rake (>= 0.8.7)
  rdoc (~> 3.4)
  thor (>= 0.17.0, < 2.0)
...

So this fact makes ri not work in a Ruby 2 + Rails 4 project at the moment… I suspect this could be the reason why docs were not included by default by RVM. Or possibly the other way around, due to the fact that docs are never included by default (so seldom used), Rails developers didn’t see there was a problem.

The easiest temporary fix is to use the full path to (the good) ri:

$ /Users/.../.rvm/rubies/ruby-2.0.0-p0/bin/ri String

Another temporary fix (I’m currently using this one) is to

  1. manually edit the Gemfile.lock such that above line reads instead
    rdoc (>= 3.4)
  2. execute
    $ bundle update rdoc

After that, you can again execute ri without specifying its path.

$ ri String