Friday, June 20, 2008

So I finally bit the bullet and did it...a merb, datamapper and couchdb lovechild.

I got up this morning, wonderful day, got in to the office early, ground up some rocket fuel coffee beans and began my development day. Started off pretty sane, but slowly it turned into a full-blown hackfest!

Suprise, suprise...look whose coding!


Yes, my Cuban bee's after finally hitting my limitations with the CouchDB 0.7.2 adapter I took one for the team and got it working for 0.8.0.

This is by no means complete as I am going to build paging into the adapter as well before I submit it for usage in DataMapper. So without any further delay...Here are the goodies. I'll again just drop in what I've overloaded so you can have a pretty seamless transition if you need it. Also, this is currently running off of the CouchDB 0.8.0 trunk (see this page for subversion linkage)

Also, note the TODO, it should be done later this evening all things considered. Last but not least, I hope this helps someone other than myself, enjoy!

module DataMapper
module Types
class Object < DataMapper::Type
primitive String
size 65535
lazy true
track :hash

def self.dump(value, property)
value.to_json unless value.nil?
end

def self.load(value, property)
value.nil? ? nil : value
end
end
end
end

module DataMapper
class Collection < LazyArray
attr_reader :_total_rows
private

def initialize(query, options = {}, &block)
assert_kind_of 'query', query, Query

unless block_given?
raise ArgumentError, 'a block must be supplied for lazy loading results', caller
end

@query = query
@key_properties = model.key(repository.name)
@_total_rows = options[:total_rows] || 0
super()
load_with(&block)
end

end
end

module DataMapper
module Adapters
class CouchDBAdapter < AbstractAdapter

def read_one(query)
doc = request do |http|
http.request(build_request(query))
end
unless doc["total_rows"] == 0
data = doc['rows'].first
query.model.load(
query.fields.map do |property|
data["value"][property.field.to_s]
end,
query)
end
end

def read_many(query)
doc = request do |http|
http.request(build_request(query))
end
Collection.new(query, {:total_rows => doc['total_rows']}) do |collection|
doc['rows'].each do |doc|
collection.load(
query.fields.map do |property|
property.typecast(doc["value"][property.field.to_s])
end
)
end
end
end
def ad_hoc_request(query)
if query.order.empty?
key = "null"
else
key = (query.order.map do |order|
"doc.#{order.property.field}"
end).join(", ")
key = "[#{key}]"
end

options = []
options << "count=#{query.limit}" if query.limit
options << "skip=#{query.offset}" if query.offset
options = options.empty? ? nil : "?#{options.join('&')}"

request = Net::HTTP::Post.new("/#{self.escaped_db_name}/_temp_view#{options}")
request["Content-Type"] = "application/json"

if query.conditions.empty?
request.body = '{"language":"javascript","map":"function(doc) { if (doc.type == \'' + query.model.name.downcase + '\') { emit(' + key + ', doc);} }"}'
else
conditions = query.conditions.map do |operator, property, value|
condition = "doc.#{property.field}"
condition << case operator
when :eql then " == '#{value}'"
when :not then " != '#{value}'"
when :gt then " > #{value}"
when :gte then " >= #{value}"
when :lt then " < #{value}"
when :lte then " <= #{value}"
when :like then like_operator(value)
end
end
request.body = '{"language":"javascript","map":"function(doc) {if (doc.type == \'' + query.model.name.downcase + '\') { if (' + conditions.join(' && ') + ') { emit(' + key + ', doc);}}}"}'
end

request
end

def request(parse_result = true, &block)
res = nil
Net::HTTP.start(@uri.host, @uri.port) do |http|
res = yield(http)
end
# debugger
JSON.parse(res.body) if parse_result
end

module Migration
def create_model_storage(repository, model)
assert_kind_of 'repository', repository, Repository
assert_kind_of 'model', model, Resource::ClassMethods

uri = "/#{self.escaped_db_name}/_design/#{model.storage_name(self.name)}"
view = Net::HTTP::Put.new(uri)
view['content_type'] = "javascript"
views = model.views.reject {|key, value| value.nil?}
# TODO: This absolutely should be handled up a level.
# You shouuld pass view a hash
#{:map => "function(doc){...}", :reduce => "function(doc){...}"}
# We'll get there...
view.body = { :views => views.each {|k,v| views[k] = {:map => v}} }.to_json

request do |http|
http.request(view)
end
end

def destroy_model_storage(repository, model)
assert_kind_of 'repository', repository, Repository
assert_kind_of 'model', model, Resource::ClassMethods

uri = "/#{self.escaped_db_name}/_design/#{model.storage_name(self.name)}"
response = http_get(uri)
unless response['error']
uri += "?rev=#{response["_rev"]}"
http_delete(uri)
end
end
end

end
end
end

No comments: