fixed a bug with the RestClient optimization, added more callbacks on the ExtendedDocument and added support for casted arrays of objects.

This commit is contained in:
Matt Aimonetti 2009-02-12 20:28:07 -08:00
parent b79bb9a912
commit 3a57ed1414
9 changed files with 277 additions and 56 deletions

View file

@ -65,4 +65,35 @@ with CouchDB in your Rails or Merb app is no harder than working with the
standard SQL alternatives. See the CouchRest::Model documentation for an
example article class that illustrates usage.
CouchRest::Model will be removed from this package.
CouchRest::Model will be removed from this package.
## CouchRest::ExtendedDocument
### Callbacks
`CouchRest::ExtendedDocuments` instances have 2 callbacks already defined for you:
`create_callback`, `save_callback`, `update_callback` and `destroy_callback`
In your document inherits from `CouchRest::ExtendedDocument`, define your callback as follows:
save_callback :before, :generate_slug_from_name
CouchRest uses a mixin you can find in lib/mixins/callbacks which is extracted from Rails 3, here are some simple usage examples:
save_callback :before, :before_method
save_callback :after, :after_method, :if => :condition
save_callback :around {|r| stuff; yield; stuff }
Check the mixin or the ExtendedDocument class to see how to implement your own callbacks.
### Casting
Often, you will want to store multiple objects within a document, to be able to retrieve your objects when you load the document,
you can define some casting rules.
property :casted_attribute, :cast_as => 'WithCastedModelMixin'
property :keywords, :cast_as => ["String"]
If you want to cast an array of instances from a specific Class, use the trick shown above ["ClassName"]

View file

@ -23,7 +23,7 @@ spec = Gem::Specification.new do |s|
s.homepage = "http://github.com/jchris/couchrest"
s.description = "CouchRest provides a simple interface on top of CouchDB's RESTful HTTP API, as well as including some utility scripts for managing views and attachments."
s.has_rdoc = true
s.authors = ["J. Chris Anderson"]
s.authors = ["J. Chris Anderson", "Matt Aimonetti"]
s.files = %w( LICENSE README.md Rakefile THANKS.md ) +
Dir["{examples,lib,spec,utils}/**/*"] -
Dir["spec/tmp"]

View file

@ -27,7 +27,7 @@ require 'couchrest/monkeypatches'
# = CouchDB, close to the metal
module CouchRest
VERSION = '0.13.1'
VERSION = '0.13.2'
autoload :Server, 'couchrest/core/server'
autoload :Database, 'couchrest/core/database'

View file

@ -40,8 +40,8 @@ module CouchRest
key = self.has_key?(property.name) ? property.name : property.name.to_sym
target = property.type
if target.is_a?(Array)
next unless self[key]
klass = ::CouchRest.constantize(target[0])
self[property.name] = self[key].collect do |value|
# Auto parse Time objects
obj = ( (property.init_method == 'new') && klass == Time) ? Time.parse(value) : klass.send(property.init_method, value)

View file

@ -56,47 +56,58 @@ module RestClient
:url => url,
:headers => headers)
end
# class Request
#
# def establish_connection(uri)
# Thread.current[:connection].finish if (Thread.current[:connection] && Thread.current[:connection].started?)
# p net_http_class
# net = net_http_class.new(uri.host, uri.port)
# net.use_ssl = uri.is_a?(URI::HTTPS)
# net.verify_mode = OpenSSL::SSL::VERIFY_NONE
# Thread.current[:connection] = net
# Thread.current[:connection].start
# Thread.current[:connection]
# end
#
# def transmit(uri, req, payload)
# setup_credentials(req)
#
# Thread.current[:host] ||= uri.host
# Thread.current[:port] ||= uri.port
#
# if (Thread.current[:connection].nil? || (Thread.current[:host] != uri.host))
# p "establishing a connection"
# establish_connection(uri)
# end
#
# display_log request_log
# http = Thread.current[:connection]
# http.read_timeout = @timeout if @timeout
#
# begin
# res = http.request(req, payload)
# rescue
# p "Net::HTTP connection failed, reconnecting"
# establish_connection(uri)
# http = Thread.current[:connection]
# require 'ruby-debug'
# debugger
# req.body_stream = nil
#
# res = http.request(req, payload)
# display_log response_log(res)
# result res
# else
# display_log response_log(res)
# process_result res
# end
#
# rescue EOFError
# raise RestClient::ServerBrokeConnection
# rescue Timeout::Error
# raise RestClient::RequestTimeout
# end
# end
class Request
def transmit(uri, req, payload)
setup_credentials(req)
Thread.current[:host] ||= uri.host
Thread.current[:port] ||= uri.port
net = net_http_class.new(uri.host, uri.port)
if Thread.current[:connection].nil? || Thread.current[:host] != uri.host
Thread.current[:connection].finish if (Thread.current[:connection] && Thread.current[:connection].started?)
net.use_ssl = uri.is_a?(URI::HTTPS)
net.verify_mode = OpenSSL::SSL::VERIFY_NONE
Thread.current[:connection] = net
Thread.current[:connection].start
end
display_log request_log
http = Thread.current[:connection]
http.read_timeout = @timeout if @timeout
begin
res = http.request(req, payload)
rescue
# p "Net::HTTP connection failed, reconnecting"
Thread.current[:connection].finish
http = Thread.current[:connection] = net
Thread.current[:connection].start
res = http.request(req, payload)
display_log response_log(res)
process_result res
else
display_log response_log(res)
process_result res
end
rescue EOFError
raise RestClient::ServerBrokeConnection
rescue Timeout::Error
raise RestClient::RequestTimeout
end
end
end

View file

@ -23,7 +23,9 @@ module CouchRest
end
# Callbacks
define_callbacks :create
define_callbacks :save
define_callbacks :update
define_callbacks :destroy
def initialize(keys={})
@ -105,12 +107,62 @@ module CouchRest
# for compatibility with old-school frameworks
alias :new_record? :new_document?
# Trigger the callbacks (before, after, around)
# and create the document
# It's important to have a create callback since you can't check if a document
# was new after you saved it
#
# When creating a document, both the create and the save callbacks will be triggered.
def create(bulk = false)
caught = catch(:halt) do
_run_create_callbacks do
_run_save_callbacks do
create_without_callbacks(bulk)
end
end
end
end
# unlike save, create returns the newly created document
def create_without_callbacks(bulk =false)
raise ArgumentError, "a document requires a database to be created to (The document or the #{self.class} default database were not set)" unless database
set_unique_id if new_document? && self.respond_to?(:set_unique_id)
result = database.save_doc(self, bulk)
(result["ok"] == true) ? self : false
end
# Creates the document in the db. Raises an exception
# if the document is not created properly.
def create!
raise "#{self.inspect} failed to save" unless self.create
end
# Trigger the callbacks (before, after, around)
# only if the document isn't new
def update(bulk = false)
caught = catch(:halt) do
if self.new_document?
save(bulk)
else
_run_update_callbacks do
_run_save_callbacks do
save_without_callbacks(bulk)
end
end
end
end
end
# Trigger the callbacks (before, after, around)
# and save the document
def save(bulk = false)
caught = catch(:halt) do
_run_save_callbacks do
save_without_callbacks(bulk)
if self.new_document?
_run_save_callbacks do
save_without_callbacks(bulk)
end
else
update(bulk)
end
end
end
@ -124,7 +176,7 @@ module CouchRest
result["ok"] == true
end
# Saves the document to the db using create or update. Raises an exception
# Saves the document to the db using save. Raises an exception
# if the document is not saved properly.
def save!
raise "#{self.inspect} failed to save" unless self.save

View file

@ -7,13 +7,22 @@ module CouchRest
# attribute to define
def initialize(name, type = nil, options = {})
@name = name.to_s
@type = type.nil? ? 'String' : type.to_s
parse_type(type)
parse_options(options)
self
end
private
def parse_type(type)
if type.nil?
@type = 'String'
else
@type = type.is_a?(Array) ? [type.first.to_s] : type.to_s
end
end
def parse_options(options)
return if options.empty?
@validation_format = options.delete(:format) if options[:format]

View file

@ -10,6 +10,7 @@ class DummyModel < CouchRest::ExtendedDocument
use_database TEST_SERVER.default_database
raise "Default DB not set" if TEST_SERVER.default_database.nil?
property :casted_attribute, :cast_as => 'WithCastedModelMixin'
property :keywords, :cast_as => ["String"]
end
describe CouchRest::CastedModel do
@ -55,6 +56,18 @@ describe CouchRest::CastedModel do
end
end
describe "casted as an array of a different type" do
before(:each) do
@obj = DummyModel.new(:keywords => ['couch', 'sofa', 'relax', 'canapé'])
end
it "should cast the array propery" do
@obj.keywords.should be_an_instance_of(Array)
@obj.keywords.first.should == 'couch'
end
end
describe "saved document with casted models" do
before(:each) do
@obj = DummyModel.new(:casted_attribute => {:name => 'whatever'})

View file

@ -10,12 +10,41 @@ describe "ExtendedDocument" do
timestamps!
end
class WithCallBacks < CouchRest::ExtendedDocument
use_database TEST_SERVER.default_database
property :name
property :run_before_save
property :run_after_save
property :run_before_create
property :run_after_create
property :run_before_update
property :run_after_update
save_callback :before do |object|
object.run_before_save = true
end
save_callback :after do |object|
object.run_after_save = true
end
create_callback :before do |object|
object.run_before_create = true
end
create_callback :after do |object|
object.run_after_create = true
end
update_callback :before do |object|
object.run_before_update = true
end
update_callback :after do |object|
object.run_after_update = true
end
end
before(:each) do
@obj = WithDefaultValues.new
end
describe "with default" do
it "should have the default value set at initalization" do
@obj.preset.should == {:right => 10, :top_align => false}
end
@ -28,7 +57,6 @@ describe "ExtendedDocument" do
end
describe "timestamping" do
it "should define the updated_at and created_at getters and set the values" do
@obj.save
obj = WithDefaultValues.get(@obj.id)
@ -36,12 +64,10 @@ describe "ExtendedDocument" do
obj.created_at.should be_an_instance_of(Time)
obj.updated_at.should be_an_instance_of(Time)
obj.created_at.to_s.should == @obj.updated_at.to_s
end
end
end
describe "saving and retrieving" do
it "should work fine" do
@obj.name = "should be easily saved and retrieved"
@obj.save
@ -57,7 +83,86 @@ describe "ExtendedDocument" do
saved_obj = WithDefaultValues.get(@obj.id)
saved_obj.set_by_proc.should be_an_instance_of(Time)
end
end
describe "callbacks" do
before(:each) do
@doc = WithCallBacks.new
end
describe "save" do
it "should not run the before filter before saving if the save failed" do
@doc.run_before_save.should be_nil
@doc.save.should be_true
@doc.run_before_save.should be_true
end
it "should not run the before filter before saving if the save failed" do
@doc.should_receive(:save).and_return(false)
@doc.run_before_save.should be_nil
@doc.save.should be_false
@doc.run_before_save.should be_nil
end
it "should run the after filter after saving" do
@doc.run_after_save.should be_nil
@doc.save.should be_true
@doc.run_after_save.should be_true
end
it "should not run the after filter before saving if the save failed" do
@doc.should_receive(:save).and_return(false)
@doc.run_after_save.should be_nil
@doc.save.should be_false
@doc.run_after_save.should be_nil
end
end
describe "create" do
it "should run the before save filter when creating" do
@doc.run_before_save.should be_nil
@doc.create.should_not be_nil
@doc.run_before_save.should be_true
end
it "should not run the before save filter when the object creation fails" do
pending "need to ask wycats about chainable callbacks" do
@doc.should_receive(:create_without_callbacks).and_return(false)
@doc.run_before_save.should be_nil
@doc.save
@doc.run_before_save.should be_nil
end
end
it "should run the before create filter" do
@doc.run_before_create.should be_nil
@doc.create.should_not be_nil
@doc.create
@doc.run_before_create.should be_true
end
it "should run the after create filter" do
@doc.run_after_create.should be_nil
@doc.create.should_not be_nil
@doc.create
@doc.run_after_create.should be_true
end
end
describe "update" do
before(:each) do
@doc.save
end
it "should run the before update filter when updating an existing document" do
@doc.run_before_update.should be_nil
@doc.update
@doc.run_before_update.should be_true
end
it "should run the after update filter when updating an existing document" do
@doc.run_after_update.should be_nil
@doc.update
@doc.run_after_update.should be_true
end
it "should run the before update filter when saving an existing document" do
@doc.run_before_update.should be_nil
@doc.save
@doc.run_before_update.should be_true
end
end
end
end