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
=>WHERE
Slice
=>LIMIT
andOFFSET
Sort
=>ORDER BY
Your translators and the Query have three special methods available at all times:
owner
clauses
stash
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:
kick
count
While two other methods are generally expected to turn the clauses
hash into something
semantically valid:
to_s
to_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#select
is called.- An
Ambition::Context
is created. - An
Ambition::Processors::Select
is created and added to the context. - The context calls
to_s
on the newSelect
processor - The block passed to
select
is 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::Select
is instantiated.- The translator’s
call
method is passed:name
, returning"users.name"
- The translator’s
==
method is passed"users.name"
and"Chris"
, returning"users.name = 'Chris'"
call
is passed:age
, returning"users.age"
==
is passed"users.age"
and22
, returning"users.age = 22"
- The translator’s
both
method 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_s
is called on the context- The context forwards this
to_s
call to an instance of the adapter’sQuery
class - The ActiveRecord adapter’s
to_s
callsto_hash
to_hash
uses theclauses
hash to build an AR hashto_s
then 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.