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 standard SQL alternatives. See the CouchRest::Model documentation for an
example article class that illustrates usage. 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.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.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.has_rdoc = true
s.authors = ["J. Chris Anderson"] s.authors = ["J. Chris Anderson", "Matt Aimonetti"]
s.files = %w( LICENSE README.md Rakefile THANKS.md ) + s.files = %w( LICENSE README.md Rakefile THANKS.md ) +
Dir["{examples,lib,spec,utils}/**/*"] - Dir["{examples,lib,spec,utils}/**/*"] -
Dir["spec/tmp"] Dir["spec/tmp"]

View file

@ -27,7 +27,7 @@ require 'couchrest/monkeypatches'
# = CouchDB, close to the metal # = CouchDB, close to the metal
module CouchRest module CouchRest
VERSION = '0.13.1' VERSION = '0.13.2'
autoload :Server, 'couchrest/core/server' autoload :Server, 'couchrest/core/server'
autoload :Database, 'couchrest/core/database' 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 key = self.has_key?(property.name) ? property.name : property.name.to_sym
target = property.type target = property.type
if target.is_a?(Array) if target.is_a?(Array)
next unless self[key]
klass = ::CouchRest.constantize(target[0]) klass = ::CouchRest.constantize(target[0])
self[property.name] = self[key].collect do |value| self[property.name] = self[key].collect do |value|
# Auto parse Time objects # Auto parse Time objects
obj = ( (property.init_method == 'new') && klass == Time) ? Time.parse(value) : klass.send(property.init_method, value) 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, :url => url,
:headers => headers) :headers => headers)
end 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 end

View file

@ -23,7 +23,9 @@ module CouchRest
end end
# Callbacks # Callbacks
define_callbacks :create
define_callbacks :save define_callbacks :save
define_callbacks :update
define_callbacks :destroy define_callbacks :destroy
def initialize(keys={}) def initialize(keys={})
@ -105,12 +107,62 @@ module CouchRest
# for compatibility with old-school frameworks # for compatibility with old-school frameworks
alias :new_record? :new_document? 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) # Trigger the callbacks (before, after, around)
# and save the document # and save the document
def save(bulk = false) def save(bulk = false)
caught = catch(:halt) do caught = catch(:halt) do
_run_save_callbacks do if self.new_document?
save_without_callbacks(bulk) _run_save_callbacks do
save_without_callbacks(bulk)
end
else
update(bulk)
end end
end end
end end
@ -124,7 +176,7 @@ module CouchRest
result["ok"] == true result["ok"] == true
end 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. # if the document is not saved properly.
def save! def save!
raise "#{self.inspect} failed to save" unless self.save raise "#{self.inspect} failed to save" unless self.save

View file

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

View file

@ -10,6 +10,7 @@ class DummyModel < CouchRest::ExtendedDocument
use_database TEST_SERVER.default_database use_database TEST_SERVER.default_database
raise "Default DB not set" if TEST_SERVER.default_database.nil? raise "Default DB not set" if TEST_SERVER.default_database.nil?
property :casted_attribute, :cast_as => 'WithCastedModelMixin' property :casted_attribute, :cast_as => 'WithCastedModelMixin'
property :keywords, :cast_as => ["String"]
end end
describe CouchRest::CastedModel do describe CouchRest::CastedModel do
@ -55,6 +56,18 @@ describe CouchRest::CastedModel do
end end
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 describe "saved document with casted models" do
before(:each) do before(:each) do
@obj = DummyModel.new(:casted_attribute => {:name => 'whatever'}) @obj = DummyModel.new(:casted_attribute => {:name => 'whatever'})

View file

@ -10,12 +10,41 @@ describe "ExtendedDocument" do
timestamps! timestamps!
end 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 before(:each) do
@obj = WithDefaultValues.new @obj = WithDefaultValues.new
end end
describe "with default" do describe "with default" do
it "should have the default value set at initalization" do it "should have the default value set at initalization" do
@obj.preset.should == {:right => 10, :top_align => false} @obj.preset.should == {:right => 10, :top_align => false}
end end
@ -28,7 +57,6 @@ describe "ExtendedDocument" do
end end
describe "timestamping" do describe "timestamping" do
it "should define the updated_at and created_at getters and set the values" do it "should define the updated_at and created_at getters and set the values" do
@obj.save @obj.save
obj = WithDefaultValues.get(@obj.id) obj = WithDefaultValues.get(@obj.id)
@ -36,12 +64,10 @@ describe "ExtendedDocument" do
obj.created_at.should be_an_instance_of(Time) obj.created_at.should be_an_instance_of(Time)
obj.updated_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 obj.created_at.to_s.should == @obj.updated_at.to_s
end end
end end
describe "saving and retrieving" do describe "saving and retrieving" do
it "should work fine" do it "should work fine" do
@obj.name = "should be easily saved and retrieved" @obj.name = "should be easily saved and retrieved"
@obj.save @obj.save
@ -57,7 +83,86 @@ describe "ExtendedDocument" do
saved_obj = WithDefaultValues.get(@obj.id) saved_obj = WithDefaultValues.get(@obj.id)
saved_obj.set_by_proc.should be_an_instance_of(Time) saved_obj.set_by_proc.should be_an_instance_of(Time)
end end
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 end