Very recently, Simon Harris had an idea: nil? for Ambition. Tasty sugar.
Let’s figure out what it takes to make
User.select { |x| x.nil? }
behave just like
User.select { |x| x == nil }
in Ambition.
Short and Sweet
Simon’s approach was to modify Ambition directly to add support for nil?. While this is for sure ambitious, nil? is just another method. Not special. The adapter should decide what to do with it.
Easy. Here’s what we added to the ActiveRecord adapter’s Select translator:
def nil?(column) left = "#{owner.table_name}.#{quote_column_name column}" negated? ? not_equal(left, nil) : self.==(left, nil) end
See it in action on lines 84 to 87.
The tests, of course, can be found in types_test.
Chaining Stuffs
So, how does this work?
Every adapter’s Select translator has a special chained_call method. Ambition invokes chained_call and passes it an array of symbols when a chained.method.call is executed on itself.
In this case, the chain is m.name.nil?. Ambition knows that m is itself and ignores it, passing [ :name, :nil? ] to chained_call.
The ActiveRecord adapter’s chained_call method takes the passed array and, if it can find the second element, sends it the first element.
Basically:
# methods = [ :name, :nil? ] if respond_to? methods[1] send(methods[1], methods.first) end
Which translates to:
self.nil? :name
Cool. Adapters don’t need to set themselves up this way, but it works for ActiveRecord.
Notice: the ActiveRecord adapter doesn’t support anything more than chains two methods deep. It calls the second element and passes the first, ignoring the rest. Almost discouraging, but chin up – this is ActiveRecord specific. Ambition itself supports chains of arbitrary length, and your adapter can, too.
So array.include?, right?
The thing is, chained_call is only invoked when a chained method call is executed on an object Ambition owns.
User.select { |x| x.nil? }
In the above, Ambition owns the x. It’s self as far as the translator is concerned.
User.select { |x| [1,2,3].include? x.id }
Ambition does not own the array, only the x.id. So what happens?
Well, it’s the same as [1,2,3] == x.id to Ambition. The dude really doesn’t care. Any time there is something like left op right, Ambition calls op(left, right) on your translator.
Here’s an idea of the call:
include?([1,2,3], x.id)
Luckily x.id is translated for you prior to this. The call really looks more like:
include?([1,2,3], 'users.id')
The include? definition, then, on ActiveRecord’s translator is very straightforward:
def include?(left, right) left = left.map { |element| sanitize element }.join(', ') "#{right} IN (#{left})" end
Beautiful.
Join the Fun
While the Err twitter is great for general stuff, you should really hop on the Ambition mailing list if you want in on this action. Or just watch the project on GitHub.
Til next time.
Nice bookmarking support in github, that’s what this was really about, wasn’t it?
Just finding Ambition, dunno what I was doing before. Does Datamapper find inspriation from this? Some of the Ambition stuff like first and each seem like Datamapper. Sorry for stating the obvious, but thanks for you efforts.
How hard would it be to implement Ambition without ParseTree/Ruby2Ruby?
Bob: Impossible!
Chime in.