Tuesday, March 11, 2008

SimpleMapper, the new lightweight and versatile ORM!

I want to point you to a "new ActiveResource" -- SimpleMapper. SimpleMapper is my complete rewrite-from-scratch of ActiveResource. Kindof like Ezra Zygmuntowicz's rewrite of rails -- more a rewrite of the entire idea itself.

SimpleMapper's basic assumption is that you have a ruby Class that you want to make Persistent. Keep your data wherever you want, all you have to do with SimpleMapper is specify how to communicate with it and in what data format. So there are only three parts to SimpleMapper apart from the Base class: Connection Adapters (communication), Format Adapters (data format), and Plugins (extra functionality).

Connection Adapters : Adapters are easy to create. My first adapter was the Http adapter so I could use it in place of ActiveResource.

Format Adapters: The data is filtered both ways through a format library of your choice - whether xml for http work or sql for a database adapter. Any other "format," even SQL, or encrypted text could be written into a format adapter.

Plugins: I now have 4 basic/default plugins. 1) 'properties' which helps to define properties and the identifier property of your objects, 2) a 'callbacks' plugin which manages callbacks for the http adapter and any other adapters that want to use callbacks (like oauth), 3) an 'oauth' plugin, which gives the SimpleMapper Http adapter oauth functionality, 4) 'association' -- experimental, to see how associations work, look at the associations_spec in the spec directory of the gem. I purposed to make them able to work across data sources, and dynamically too.

Usage

This example demonstrates using the simple_model and OAuth plugins. The session method is provided to emulate the controller's part in the game. OAuth needs to access the session -- it will run the session method on the object given to set_oauth, as you see in "Person.set_oauth(self)".
CONFIG = {
:consumer_key => "lSwfFZ4n4MB7k457ZZMiGnsRIwL2O4j9oIGN42n3",
:consumer_secret => "p64RuLYJox1C9nrhXGmUlC0h38ruS5GHVlVGsgNZ"
}

def session
Hash.new {|h,k| h[k] = {}}
end

require 'simple_mapper'

class Person < SimpleMapper::Base
# Use the properties plugin, so we can use properties parsed from the RESTful entity.
has :properties

uses :oauth

# Load the parsing engine: JSON
set_format :json

# Load the connection adapter we want: HTTP
add_connection_adapter(:default, :http) {
# This block is run in the context of the adapter, so here we set up the adapter.
set_base_url CONFIG['people_url']

# Set up the OAuth within the adapter, where it belongs.
requires_oauth(
CONFIG['consumer_key'],
CONFIG['consumer_secret'],
{ :site => base_url.to_s,
:request_token_path => "/request_token",
:access_token_path => "/access_token",
:authorize_path => "/authorize",
:auth_method => :query,
:authorization_method => :scriptable,
:session => lambda {session['people_oauth_session']}
}
)
}

# Now we can define the properties for the model.
properties :self,
:first_name,
:last_name,
:created_by,
:created_at,
:updated_at,
:birthdate,
:phone_number,
:email
# And the identifier. The properties plugin assumes there are several properties, and one of them is the identifier.
identifier :self
end

# Run the following line in the controller. If your OAuth is scriptable, i.e. it has keys that are already valid, it will simply set up the OAuth from the session. If you need authorization from the user, you'll need to define the "begin_pathway" method in your controller. (Should call 'redirect' for the user and store the state of things and where the user is heading, so when they get back you know what to do with them.)

Person.connection(:default).set_oauth(self)

# - - - In the console... - - -

>> @person = Person.new(:first_name => 'Bobby')
=> #<Person:0x1157740 @first_name="Bobby">
>> @person.save
=> #<Person:0x1157740 @first_name="Bobby", @self="http://localhost:3000/people/20">
>> @people = Person.get
=> [#<Person:0x110de9c @first_name="Daniel", @persisted=true, @self="http://localhost:3000/people/1">, #<Person:0x110dbcc @first_name="Jon", @persisted=true, @self="http://localhost:3000/people/2">, . . . ]
>> @people[-1].delete
=> nil
>> @people[2].first_name = 'Jo'
=> "Jo"
>> @people[2].save
=> #<Person:0x110d8fc @first_name="Jo", @persisted=true, @self="http://localhost:3000/people/16">
>> @people[2].delete
=> true
>> @people[2].save
=> #<Person:0x110d8fc @first_name="Jo", @persisted=true, @self="http://localhost:3000/people/21">
The Person.connection(:default).set_oauth(self) is, of course, only important for OAuth Authentication. Dig into the source for that method if you want to see what it actually does. It's just a macro to begin or complete the OAuth Authentication sequence, which includes redirecting to the provider site to allow the user to grant permission to his/her data. If you don't know anything about this sequence, I strongly recommend reading about and becoming familiar with OAuth.

Summary

In my usage so far, it's much easier to use than ActiveResource. It just does things right. It's lightweight, so I don't have to try to figure out what's going on. It doesn't rely on the bloated ActiveSupport package that messes with so many core classes. I can explicitly define how my model should be persistent, to the T, yet without too much configuration.

To install, just type:
gem install simplemapper
in your console.

7 comments:

dkubb said...

Have you looked at DataMapper (DM) at all? The plan is to eventually make it so DM models can access not only databases, but resources too like ActiveResource.

If you're interested check out http://datamapper.org and #datamapper on irc.freenode.net

daniel said...

Yes, I use DataMapper for everything now. I didn't see the support for other resources coming anytime soon, and I needed this now.

I'm adding support in the next version for more than one adapter per model, so you can pull the same objects from a database or from http, and it'll remember the source each object is from.

I also like how light it is. Even DataMapper works with attributes and associations builtin. I wanted something light enough I can only include what I need. In one day's work I made an ActiveResource clone complete with xml parsing and OpenAuthorization.

I also like the more explicit de-coupling of the adapters and format conversion engines. It's easy to make your own adapter or format conversion.

Yes, I like DataMapper, but this fills the niche of Small, Light, and Simple.

JK said...

May I ask where is the "simple_model"? It doesn't seem to be under default_plugins or anywhere else in the gem for that matter.

JK said...

Thanks for the update!
...though I still couldn't get the example to work.
Where should the line "Person.set_oauth(self)" be put? A persons controller? Either way, rails keeps complaining that 'set_oauth' is an undefined method.

Just some other minor things, you might like to change the APP hash to CONFIG in your example.
Also, the gem doesn't have any dependency specified (formattedstring & hash_magic). You might like to take a look at that as well.

blog said...

Since this post I made things better... Here's some new information:

has, uses, acts_as, and include_plugin are all aliases of the same thing: "load this plugin" - just use the verbage that suits you for the plugins you want.

.set_oauth is now run on the adapter instead of the model itself. It only makes sense since you can have multiple adapters in one model and it's really a plugin on the http adapter, not the model.

Thanks for the suggestions! Hope you can get it figured out! If you still have any problems, email me at gems@behindlogic.com and I'll help you out and find what I need to update on the web or in the documentation.

Anonymous said...

Your gem doesn't list it's dependencies (hash_magic, formattedstring, etc.)

daniel said...

Yeah, this was one of my first gems. I've updated a new release on rubyforge (not sure if it worked properly). I also just now imported the project into github: http://github.com/dcparker/simplemapper. Follow the project there if you want, fork and develop further, or clone it and run "rake package" to get a gem out of it.