Ambition
The Adapters
Adapters are gems named ambitious-something, where something corresponds to the data
store they are adapting. They can be required in code via ambition/adapters/something.
To install and test the ActiveRecord adapter:
$ gem install ambitious-activerecord $ irb >> require 'rubygems' >> require 'ambition/adapters/active_record'
Adapters typically inject themselves into their target automatically, so that should be all you need.
There are a few adapters in development or released currently:
- ActiveRecord
- ActiveLDAP
- XPath
- CouchDB
- DataMapper
If you’re interested in writing your own adapter, read on.
The Anatomy of an Adapter
Ambition adapters consist of two parts: Translators and the Query. Translators are used to translate plane jane Ruby into strings while the Query is used to build and execute a query from those strings.
The three translators are Select, Slice, and Sort. Their names correspond to the
API method they represent. Each translator consists of methods which convert passed
arguments into a string.
Here’s how the ActiveRecord adapter maps translator classes to SQL clauses:
Select=>WHERESlice=>LIMITandOFFSETSort=>ORDER BY
Your translators and the Query have three special methods available at all times:
ownerclausesstash
owner is the class from which the request was generated.
User.select { |u| u.name == 'Pork' }
# => owner == User
clauses is the hash of translated string arrays, keyed by processors.
User.select { |u| u.name == 'Pork' }
# => clauses == { :select => [ "users.name = 'Pork'" ] }
stash is your personal private stash. A hash you can use for
keeping stuff around. Translators are free to set things which
can later be picked up by the Query class.
For instance, the ActiveRecord adapter’s Select translator adds to the
stash[:include] array whenever it thinks it needs to do a join. The
Query class picks this up and adds it to the hash it feeds
find(:all).
User.select { |u| u.profile.name == 'Pork' }
# => stash == { :include => [ :profile ] }
stash is basically a way for your translators to talk to each other and,
more importantly, to the Query.
The Query is what kicks off the actual data store query, after all the translators have done
their business. Its clauses and stash hashes are as full as they will ever be.
The kicking is done by one of two methods:
kickcount
While two other methods are generally expected to turn the clauses hash into something
semantically valid:
to_sto_hash
So, for instance, Query#kick may look like this:
def kick owner.find(:all, to_hash) end
A straightforward translator/query API reference can be found on the api page.
The easiest way to understand translators is to check out the source of the existing adapters or by using the adapter generator.
The Adapter Generator
Ambition ships with an ambition_adapter script which can generate an adapter skeleton. Built
using Dr Nic’s Rubigen, it spits out all the files, tests, and
Rakefiles your adapter needs.
Run it:
$ ambition_adapter flickr
create
create lib/ambition/adapters/flickr
create test
create lib/ambition/adapters/flickr/base.rb
create lib/ambition/adapters/flickr/query.rb
create lib/ambition/adapters/flickr/select.rb
create lib/ambition/adapters/flickr/slice.rb
create lib/ambition/adapters/flickr/sort.rb
create lib/ambition/adapters/flickr.rb
create test/helper.rb
create test/select_test.rb
create test/slice_test.rb
create test/sort_test.rb
create README
create Rakefile
Presto, you’ve got a ready and willing adapter skeleton in place now. Check out the comments and you’re on your way.
The Flow: Ambition + Adapters
Let us examine the flow of a typical call, using the ActiveRecord adapter for reference.
The call:
User.select { |u| u.name == 'Chris' && u.age == 22 }.to_s
The first few steps:
Ambition::API#selectis called.- An
Ambition::Contextis created. - An
Ambition::Processors::Selectis created and added to the context. - The context calls
to_son the newSelectprocessor - The block passed to
selectis processed.
This processing is the real meat. Ambition will instantiate your Select translator and
pass values to it, saving the return value of these method calls.
returning "(users.name = 'Chris' AND users.age = 22)"
Ambition::Adapters::ActiveRecord::Selectis instantiated.- The translator’s
callmethod is passed:name, returning"users.name" - The translator’s
==method is passed"users.name"and"Chris", returning"users.name = 'Chris'" callis passed:age, returning"users.age"==is passed"users.age"and22, returning"users.age = 22"- The translator’s
bothmethod is passed"users.name = 'Chris'"and"users.age = 22",
At this point we leave adapter-land. The final string is stored in the clauses hash
(available to your Query) by the context. The clauses hash is keyed by the translator
name—in this case, :select.
- The context is returned by
Ambition::API#select. to_sis called on the context- The context forwards this
to_scall to an instance of the adapter’sQueryclass - The ActiveRecord adapter’s
to_scallsto_hash to_hashuses theclauseshash to build an AR hashto_sthen uses the hash’s members to build a SQL string
The final string is then returned:
"SELECT * FROM users WHERE (users.name = 'Chris' AND users.age = 22)"
And that’s all there is to it. Except, of course, for the api page.