From a78e3b74d650048b5d5f58e0d9e99e1175076d96 Mon Sep 17 00:00:00 2001 From: Sam Lown Date: Wed, 9 Feb 2011 21:21:03 +0100 Subject: [PATCH] Adding support for proxying and more refinements to views --- lib/couchrest/model/associations.rb | 4 +- lib/couchrest/model/base.rb | 4 + lib/couchrest/model/collection.rb | 2 +- lib/couchrest/model/designs/view.rb | 80 +++-- lib/couchrest/model/document_queries.rb | 2 +- lib/couchrest/model/persistence.rb | 6 +- lib/couchrest/model/proxyable.rb | 152 +++++++++ lib/couchrest/model/validations/uniqueness.rb | 11 +- lib/couchrest/model/views.rb | 2 +- lib/couchrest_model.rb | 1 + spec/couchrest/base_spec.rb | 8 +- spec/couchrest/designs/view_spec.rb | 27 +- spec/couchrest/persistence_spec.rb | 6 +- spec/couchrest/proxyable_spec.rb | 301 ++++++++++++++++++ 14 files changed, 560 insertions(+), 46 deletions(-) create mode 100644 lib/couchrest/model/proxyable.rb create mode 100644 spec/couchrest/proxyable_spec.rb diff --git a/lib/couchrest/model/associations.rb b/lib/couchrest/model/associations.rb index 889ff5c..ac02fda 100644 --- a/lib/couchrest/model/associations.rb +++ b/lib/couchrest/model/associations.rb @@ -109,7 +109,7 @@ module CouchRest base = options[:proxy] || options[:class_name] class_eval <<-EOS, __FILE__, __LINE__ + 1 def #{attrib} - @#{attrib} ||= #{options[:foreign_key]}.nil? ? nil : #{base}.get(self.#{options[:foreign_key]}) + @#{attrib} ||= #{options[:foreign_key]}.nil? ? nil : (model_proxy || #{base}).get(self.#{options[:foreign_key]}) end EOS end @@ -140,7 +140,7 @@ module CouchRest class_eval <<-EOS, __FILE__, __LINE__ + 1 def #{attrib}(reload = false) return @#{attrib} unless @#{attrib}.nil? or reload - ary = self.#{options[:foreign_key]}.collect{|i| #{base}.get(i)} + ary = self.#{options[:foreign_key]}.collect{|i| (model_proxy || #{base}).get(i)} @#{attrib} = ::CouchRest::CollectionOfProxy.new(ary, self, '#{options[:foreign_key]}') end EOS diff --git a/lib/couchrest/model/base.rb b/lib/couchrest/model/base.rb index b49c675..330f4db 100644 --- a/lib/couchrest/model/base.rb +++ b/lib/couchrest/model/base.rb @@ -12,6 +12,7 @@ module CouchRest include CouchRest::Model::DesignDoc include CouchRest::Model::ExtendedAttachments include CouchRest::Model::ClassProxy + include CouchRest::Model::Proxyable include CouchRest::Model::Collection include CouchRest::Model::PropertyProtection include CouchRest::Model::Associations @@ -46,9 +47,12 @@ module CouchRest # Options supported: # # * :directly_set_attributes: true when data comes directly from database + # * :database: provide an alternative database # def initialize(doc = {}, options = {}) doc = prepare_all_attributes(doc, options) + # set the instances database, if provided + self.database = options[:database] unless options[:database].nil? super(doc) unless self['_id'] && self['_rev'] self[self.model_type_key] = self.class.to_s diff --git a/lib/couchrest/model/collection.rb b/lib/couchrest/model/collection.rb index f12c7ec..d4633f1 100644 --- a/lib/couchrest/model/collection.rb +++ b/lib/couchrest/model/collection.rb @@ -222,7 +222,7 @@ module CouchRest if @container_class.nil? results else - results['rows'].collect { |row| @container_class.create_from_database(row['doc']) } unless results['rows'].nil? + results['rows'].collect { |row| @container_class.build_from_database(row['doc']) } unless results['rows'].nil? end end diff --git a/lib/couchrest/model/designs/view.rb b/lib/couchrest/model/designs/view.rb index 55bc055..7708f07 100644 --- a/lib/couchrest/model/designs/view.rb +++ b/lib/couchrest/model/designs/view.rb @@ -5,18 +5,19 @@ module CouchRest # # A proxy class that allows view queries to be created using # chained method calls. After each call a new instance of the method - # is created based on the original in a similar fashion to ruby's sequel + # is created based on the original in a similar fashion to ruby's Sequel # library, or Rails 3's Arel. # # CouchDB views have inherent limitations, so joins and filters as used in - # a normal relational database are not possible. At least not yet! + # a normal relational database are not possible. # class View include Enumerable attr_accessor :model, :name, :query, :result - # Initialize a new View object. This method should not be called from outside CouchRest Model. + # Initialize a new View object. This method should not be called from + # outside CouchRest Model. def initialize(parent, new_query = {}, name = nil) if parent.is_a?(Class) && parent < CouchRest::Model::Base raise "Name must be provided for view to be initialized" if name.nil? @@ -25,14 +26,14 @@ module CouchRest # Default options: self.query = { :reduce => false } elsif parent.is_a?(self.class) - self.model = parent.model + self.model = (new_query.delete(:proxy) || parent.model) self.name = parent.name self.query = parent.query.dup else raise "View cannot be initialized without a parent Model or View" end query.update(new_query) - super + super() end @@ -110,6 +111,13 @@ module CouchRest end end + # Check to see if the array of documents is empty. This *will* + # perform the query and return all documents ready to use, if you don't + # want to load anything, use +#total_rows+ or +#count+ instead. + def empty? + all.empty? + end + # Run through each document provided by the +#all+ method. # This is also used by the Enumerator mixin to provide all the standard # ruby collection directly on the view. @@ -141,6 +149,18 @@ module CouchRest rows.map{|r| r.value} end + # Accept requests as if the view was an array. Used for backwards compatibity + # with older queries: + # + # Model.all(:raw => true, :limit => 0)['total_rows'] + # + # In this example, the raw option will be ignored, and the total rows + # will still be accessible. + # + def [](value) + execute[value] + end + # No yet implemented. Eventually this will provide a raw hash # of the information CouchDB holds about the view. def info @@ -150,16 +170,11 @@ module CouchRest # == View Filter Methods # - # View filters return an copy of the view instance with the query + # View filters return a copy of the view instance with the query # modified appropriatly. Errors will be raised if the methods # are combined in an incorrect fashion. # - # Specify the database the view should use. If not defined, - # an attempt will be made to load its value from the model. - def database(value) - update_query(:database => value) - end # Find all entries in the index whose key matches the value provided. # @@ -229,7 +244,8 @@ module CouchRest update_query(:skip => value) end - # Use the reduce function on the view. If none is available this method will fail. + # Use the reduce function on the view. If none is available this method + # will fail. def reduce raise "Cannot reduce a view without a reduce method" unless can_reduce? update_query(:reduce => true) @@ -257,6 +273,25 @@ module CouchRest update_query.include_docs! end + ### Special View Filter Methods + + # Specify the database the view should use. If not defined, + # an attempt will be made to load its value from the model. + def database(value) + update_query(:database => value) + end + + # Set the view's proxy that will be used instead of the model + # for any future searches. As soon as this enters the + # new object's initializer it will be removed and replace + # the model object. + # + # See the Proxyable mixin for more details. + # + def proxy(value) + update_query(:proxy => value) + end + # Return any cached values to their nil state so that any queries # requested later will have a fresh set of data. def reset! @@ -288,20 +323,22 @@ module CouchRest def can_reduce? !design_doc['views'][name]['reduce'].blank? end - + + def use_database + query[:database] || model.database + end def execute return self.result if result - db = query[:database] || model.database - raise "Database must be defined in model or view!" if db.nil? + raise "Database must be defined in model or view!" if use_database.nil? retryable = true # Remove the reduce value if its not needed query.delete(:reduce) unless can_reduce? begin - self.result = model.design_doc.view_on(db, name, query) + self.result = model.design_doc.view_on(use_database, name, query) rescue RestClient::ResourceNotFound => e if retryable - model.save_design_doc(db) + model.save_design_doc(use_database) retryable = false retry else @@ -334,9 +371,10 @@ module CouchRest def create(model, name, opts = {}) unless opts[:map] - if opts[:by].nil? && name =~ /^by_(.+)/ + if opts[:by].nil? && name.to_s =~ /^by_(.+)/ opts[:by] = $1.split(/_and_/) end + raise "View cannot be created without recognised name, :map or :by options" if opts[:by].nil? opts[:guards] ||= [] @@ -373,9 +411,9 @@ module CouchRest # A special wrapper class that provides easy access to the key # fields in a result row. class ViewRow < Hash - attr_accessor :model + attr_reader :model def initialize(hash, model) - self.model = model + @model = model replace(hash) end def id @@ -393,7 +431,7 @@ module CouchRest # Send a request for the linked document either using the "id" field's # value, or the ["value"]["_id"] used for linked documents. def doc - return model.create_from_database(self['doc']) if self['doc'] + return model.build_from_database(self['doc']) if self['doc'] doc_id = (value.is_a?(Hash) && value['_id']) ? value['_id'] : self.id model.get(doc_id) end diff --git a/lib/couchrest/model/document_queries.rb b/lib/couchrest/model/document_queries.rb index 575a675..af1083d 100644 --- a/lib/couchrest/model/document_queries.rb +++ b/lib/couchrest/model/document_queries.rb @@ -88,7 +88,7 @@ module CouchRest def get!(id, db = database) raise "Missing or empty document ID" if id.to_s.empty? doc = db.get id - create_from_database(doc) + build_from_database(doc) end alias :find! :get! diff --git a/lib/couchrest/model/persistence.rb b/lib/couchrest/model/persistence.rb index 372148d..fbc35ba 100644 --- a/lib/couchrest/model/persistence.rb +++ b/lib/couchrest/model/persistence.rb @@ -93,12 +93,12 @@ module CouchRest # Creates a new instance, bypassing attribute protection # - # # ==== Returns # a document instance - def create_from_database(doc = {}) + # + def build_from_database(doc = {}) base = (doc[model_type_key].blank? || doc[model_type_key] == self.to_s) ? self : doc[model_type_key].constantize - base.new(doc, :directly_set_attributes => true) + base.new(doc, :directly_set_attributes => true) end # Defines an instance and save it directly to the database diff --git a/lib/couchrest/model/proxyable.rb b/lib/couchrest/model/proxyable.rb new file mode 100644 index 0000000..d7fec3d --- /dev/null +++ b/lib/couchrest/model/proxyable.rb @@ -0,0 +1,152 @@ +module CouchRest + module Model + # :nodoc: Because I like inventing words + module Proxyable + extend ActiveSupport::Concern + + included do + attr_accessor :model_proxy + end + + module ClassMethods + + # Define a collection that will use the base model for the database connection + # details. + def proxy_for(model_name, options = {}) + db_method = options[:database_method] || "proxy_database" + options[:class_name] ||= model_name.to_s.singularize.camelize + class_eval <<-EOS, __FILE__, __LINE__ + 1 + def #{model_name} + unless respond_to?('#{db_method}') + raise "Missing ##{db_method} method for proxy" + end + @#{model_name} ||= CouchRest::Model::Proxyable::ModelProxy.new(#{options[:class_name]}, self, '#{model_name}', #{db_method}) + end + EOS + end + + def proxied_by(model_name, options = {}) + raise "Model can only be proxied once or ##{model_name} already defined" if method_defined?(model_name) + attr_accessor model_name + end + end + + class ModelProxy + + attr_reader :model, :owner, :owner_name, :database + + def initialize(model, owner, owner_name, database) + @model = model + @owner = owner + @owner_name = owner_name + @database = database + end + + # Base + + def new(*args) + proxy_update(model.new(*args)) + end + + def build_from_database(doc = {}) + proxy_update(model.build_from_database(doc)) + end + + def method_missing(m, *args, &block) + if has_view?(m) + if model.respond_to?(m) + return model.send(m, *args).proxy(self) + else + query = args.shift || {} + return view(m, query, *args, &block) + end + elsif m.to_s =~ /^find_(by_.+)/ + view_name = $1 + if has_view?(view_name) + return first_from_view(view_name, *args) + end + end + super + end + + # DocumentQueries + + def all(opts = {}, &block) + proxy_update_all(@model.all({:database => @database}.merge(opts), &block)) + end + + def count(opts = {}) + @model.count({:database => @database}.merge(opts)) + end + + def first(opts = {}) + proxy_update(@model.first({:database => @database}.merge(opts))) + end + + def last(opts = {}) + proxy_update(@model.last({:database => @database}.merge(opts))) + end + + def get(id) + proxy_update(@model.get(id, @database)) + end + alias :find :get + + # Views + + def has_view?(view) + @model.has_view?(view) + end + + def view_by(*args) + @model.view_by(*args) + end + + def view(name, query={}, &block) + proxy_update_all(@model.view(name, {:database => @database}.merge(query), &block)) + end + + def first_from_view(name, *args) + # add to first hash available, or add to end + (args.last.is_a?(Hash) ? args.last : (args << {}).last)[:database] = @database + proxy_update(@model.first_from_view(name, *args)) + end + + # DesignDoc + + def design_doc + @model.design_doc + end + + def refresh_design_doc + @model.refresh_design_doc(@database) + end + + def save_design_doc + @model.save_design_doc(@database) + end + + + protected + + # Update the document's proxy details, specifically, the fields that + # link back to the original document. + def proxy_update(doc) + if doc + doc.database = @database if doc.respond_to?(:database=) + doc.model_proxy = self if doc.respond_to?(:model_proxy=) + doc.send("#{owner_name}=", owner) if doc.respond_to?("#{owner_name}=") + end + doc + end + + def proxy_update_all(docs) + docs.each do |doc| + proxy_update(doc) + end + end + + end + end + end +end diff --git a/lib/couchrest/model/validations/uniqueness.rb b/lib/couchrest/model/validations/uniqueness.rb index 755cdf3..76777c7 100644 --- a/lib/couchrest/model/validations/uniqueness.rb +++ b/lib/couchrest/model/validations/uniqueness.rb @@ -9,19 +9,19 @@ module CouchRest # Ensure we have a class available so we can check for a usable view # or add one if necessary. - def setup(klass) - @klass = klass + def setup(model) + @model = model end - def validate_each(document, attribute, value) view_name = options[:view].nil? ? "by_#{attribute}" : options[:view] + model = document.model_proxy || @model # Determine the base of the search - base = options[:proxy].nil? ? @klass : document.instance_eval(options[:proxy]) + base = options[:proxy].nil? ? model : document.instance_eval(options[:proxy]) if base.respond_to?(:has_view?) && !base.has_view?(view_name) raise "View #{document.class.name}.#{options[:view]} does not exist!" unless options[:view].nil? - @klass.view_by attribute + model.view_by attribute end docs = base.view(view_name, :key => value, :limit => 2, :include_docs => false)['rows'] @@ -36,7 +36,6 @@ module CouchRest end end - end end diff --git a/lib/couchrest/model/views.rb b/lib/couchrest/model/views.rb index 74a8660..a7bdd92 100644 --- a/lib/couchrest/model/views.rb +++ b/lib/couchrest/model/views.rb @@ -131,7 +131,7 @@ module CouchRest collection_proxy_for(design_doc, name, opts.merge({:database => db, :include_docs => true})) else view = fetch_view db, name, opts.merge({:include_docs => true}), &block - view['rows'].collect{|r|create_from_database(r['doc'])} if view['rows'] + view['rows'].collect{|r|build_from_database(r['doc'])} if view['rows'] end end end diff --git a/lib/couchrest_model.rb b/lib/couchrest_model.rb index bc5faf5..f43ecba 100644 --- a/lib/couchrest_model.rb +++ b/lib/couchrest_model.rb @@ -37,6 +37,7 @@ require "couchrest/model/views" require "couchrest/model/design_doc" require "couchrest/model/extended_attachments" require "couchrest/model/class_proxy" +require "couchrest/model/proxyable" require "couchrest/model/collection" require "couchrest/model/associations" require "couchrest/model/configuration" diff --git a/spec/couchrest/base_spec.rb b/spec/couchrest/base_spec.rb index d64641c..9af6037 100644 --- a/spec/couchrest/base_spec.rb +++ b/spec/couchrest/base_spec.rb @@ -34,10 +34,16 @@ describe "Model Base" do @obj.should be_new_record end - it "should not failed on a nil value in argument" do + it "should not fail with nil argument" do @obj = Basic.new(nil) @obj.should_not be_nil end + + it "should allow the database to be set" do + @obj = Basic.new(nil, :database => 'database') + @obj.database.should eql('database') + end + end describe "ActiveModel compatability Basic" do diff --git a/spec/couchrest/designs/view_spec.rb b/spec/couchrest/designs/view_spec.rb index 39b0dbc..df1f063 100644 --- a/spec/couchrest/designs/view_spec.rb +++ b/spec/couchrest/designs/view_spec.rb @@ -194,6 +194,15 @@ describe "Design View" do end end + describe "#empty?" do + it "should check the #all method for any results" do + all = mock("All") + all.should_receive(:empty?).and_return('win') + @obj.should_receive(:all).and_return(all) + @obj.empty?.should eql('win') + end + end + describe "#each" do it "should call each method on all" do @obj.should_receive(:all).and_return([]) @@ -242,6 +251,13 @@ describe "Design View" do end end + describe "#[]" do + it "should execute and provide requested field" do + @obj.should_receive(:execute).and_return({'total_rows' => 2}) + @obj['total_rows'].should eql(2) + end + end + describe "#info" do it "should raise error" do lambda { @obj.info }.should raise_error @@ -509,10 +525,8 @@ describe "Design View" do it "should retry once on a resource not found error" do @obj.should_receive(:can_reduce?).and_return(true) @obj.model.should_receive(:save_design_doc) - @design_doc.should_receive(:view_on).ordered - .and_raise(RestClient::ResourceNotFound) - @design_doc.should_receive(:view_on).ordered - .and_return('foos') + @design_doc.should_receive(:view_on).ordered.and_raise(RestClient::ResourceNotFound) + @design_doc.should_receive(:view_on).ordered.and_return('foos') @obj.send(:execute) @obj.result.should eql('foos') end @@ -520,8 +534,7 @@ describe "Design View" do it "should retry twice and fail on a resource not found error" do @obj.should_receive(:can_reduce?).and_return(true) @obj.model.should_receive(:save_design_doc) - @design_doc.should_receive(:view_on).twice - .and_raise(RestClient::ResourceNotFound) + @design_doc.should_receive(:view_on).twice.and_raise(RestClient::ResourceNotFound) lambda { @obj.send(:execute) }.should raise_error(RestClient::ResourceNotFound) end @@ -577,7 +590,7 @@ describe "Design View" do hash = {'doc' => {'_id' => '12345', 'name' => 'sam'}} obj = @klass.new(hash, DesignViewModel) doc = mock('DesignViewModel') - obj.model.should_receive(:create_from_database).with(hash['doc']).and_return(doc) + obj.model.should_receive(:build_from_database).with(hash['doc']).and_return(doc) obj.doc.should eql(doc) end diff --git a/spec/couchrest/persistence_spec.rb b/spec/couchrest/persistence_spec.rb index 01a3b86..8e360d9 100644 --- a/spec/couchrest/persistence_spec.rb +++ b/spec/couchrest/persistence_spec.rb @@ -15,17 +15,17 @@ describe "Model Persistence" do describe "creating a new document from database" do it "should instantialize" do - doc = Article.create_from_database({'_id' => 'testitem1', '_rev' => 123, 'couchrest-type' => 'Article', 'name' => 'my test'}) + doc = Article.build_from_database({'_id' => 'testitem1', '_rev' => 123, 'couchrest-type' => 'Article', 'name' => 'my test'}) doc.class.should eql(Article) end it "should instantialize of same class if no couchrest-type included from DB" do - doc = Article.create_from_database({'_id' => 'testitem1', '_rev' => 123, 'name' => 'my test'}) + doc = Article.build_from_database({'_id' => 'testitem1', '_rev' => 123, 'name' => 'my test'}) doc.class.should eql(Article) end it "should instantialize document of different type" do - doc = Article.create_from_database({'_id' => 'testitem2', '_rev' => 123, Article.model_type_key => 'WithTemplateAndUniqueID', 'name' => 'my test'}) + doc = Article.build_from_database({'_id' => 'testitem2', '_rev' => 123, Article.model_type_key => 'WithTemplateAndUniqueID', 'name' => 'my test'}) doc.class.should eql(WithTemplateAndUniqueID) end diff --git a/spec/couchrest/proxyable_spec.rb b/spec/couchrest/proxyable_spec.rb new file mode 100644 index 0000000..de2911b --- /dev/null +++ b/spec/couchrest/proxyable_spec.rb @@ -0,0 +1,301 @@ +require File.expand_path("../../spec_helper", __FILE__) + +require File.join(FIXTURE_PATH, 'more', 'cat') + +class DummyProxyable < CouchRest::Model::Base + def proxy_database + 'db' # Do not use this! + end +end + +class ProxyKitten < CouchRest::Model::Base +end + +describe "Proxyable" do + + it "should provide #model_proxy method" do + DummyProxyable.new.should respond_to(:model_proxy) + end + + describe "class methods" do + + describe ".proxy_for" do + + it "should be provided" do + DummyProxyable.should respond_to(:proxy_for) + end + + it "should create a new method" do + DummyProxyable.stub!(:method_defined?).and_return(true) + DummyProxyable.proxy_for(:cats) + DummyProxyable.new.should respond_to(:cats) + end + + describe "generated method" do + it "should call ModelProxy" do + DummyProxyable.proxy_for(:cats) + @obj = DummyProxyable.new + CouchRest::Model::Proxyable::ModelProxy.should_receive(:new).with(Cat, @obj, 'cats', 'db').and_return(true) + @obj.should_receive('proxy_database').and_return('db') + @obj.should_receive(:respond_to?).with('proxy_database').and_return(true) + @obj.cats + end + + it "should raise an error if the database method is missing" do + DummyProxyable.proxy_for(:cats) + @obj = DummyProxyable.new + @obj.should_receive(:respond_to?).with('proxy_database').and_return(false) + lambda { @obj.cats }.should raise_error(StandardError, "Missing #proxy_database method for proxy") + end + + it "should raise an error if custom database method missing" do + DummyProxyable.proxy_for(:proxy_kittens, :database_method => "foobardom") + @obj = DummyProxyable.new + lambda { @obj.proxy_kittens }.should raise_error(StandardError, "Missing #foobardom method for proxy") + end + + end + + end + + describe ".proxied_by" do + it "should be provided" do + DummyProxyable.should respond_to(:proxied_by) + end + + it "should add an attribute accessor" do + DummyProxyable.proxied_by(:foobar) + DummyProxyable.new.should respond_to(:foobar) + end + + it "should raise an error if model name pre-defined" do + lambda { DummyProxyable.proxied_by(:object_id) }.should raise_error + end + end + + end + + describe "ModelProxy" do + + before :all do + @klass = CouchRest::Model::Proxyable::ModelProxy + end + + it "should initialize and set variables" do + @obj = @klass.new(Cat, 'owner', 'owner_name', 'database') + @obj.model.should eql(Cat) + @obj.owner.should eql('owner') + @obj.owner_name.should eql('owner_name') + @obj.database.should eql('database') + end + + describe "instance" do + + before :each do + @obj = @klass.new(Cat, 'owner', 'owner_name', 'database') + end + + it "should proxy new call" do + Cat.should_receive(:new).and_return({}) + @obj.should_receive(:proxy_update).and_return(true) + @obj.new + end + + it "should proxy build_from_database" do + Cat.should_receive(:build_from_database).and_return({}) + @obj.should_receive(:proxy_update).with({}).and_return(true) + @obj.build_from_database + end + + describe "#method_missing" do + it "should return design view object" do + m = "by_some_property" + inst = mock('DesignView') + inst.stub!(:proxy).and_return(inst) + @obj.should_receive(:has_view?).with(m).and_return(true) + Cat.should_receive(:respond_to?).with(m).and_return(true) + Cat.should_receive(:send).with(m).and_return(inst) + @obj.method_missing(m).should eql(inst) + end + + it "should call view if necessary" do + m = "by_some_property" + @obj.should_receive(:has_view?).with(m).and_return(true) + Cat.should_receive(:respond_to?).with(m).and_return(false) + @obj.should_receive(:view).with(m, {}).and_return('view') + @obj.method_missing(m).should eql('view') + end + + it "should provide wrapper for #first_from_view" do + m = "find_by_some_property" + view = "by_some_property" + @obj.should_receive(:has_view?).with(m).and_return(false) + @obj.should_receive(:has_view?).with(view).and_return(true) + @obj.should_receive(:first_from_view).with(view).and_return('view') + @obj.method_missing(m).should eql('view') + end + + end + + it "should proxy #all" do + Cat.should_receive(:all).with({:database => 'database'}) + @obj.should_receive(:proxy_update_all) + @obj.all + end + + it "should proxy #count" do + Cat.should_receive(:all).with({:database => 'database', :raw => true, :limit => 0}).and_return({'total_rows' => 3}) + @obj.count.should eql(3) + end + + it "should proxy #first" do + Cat.should_receive(:first).with({:database => 'database'}) + @obj.should_receive(:proxy_update) + @obj.first + end + + it "should proxy #last" do + Cat.should_receive(:last).with({:database => 'database'}) + @obj.should_receive(:proxy_update) + @obj.last + end + + it "should proxy #get" do + Cat.should_receive(:get).with(32, 'database') + @obj.should_receive(:proxy_update) + @obj.get(32) + end + it "should proxy #find" do + Cat.should_receive(:get).with(32, 'database') + @obj.should_receive(:proxy_update) + @obj.find(32) + end + + it "should proxy #has_view?" do + Cat.should_receive(:has_view?).with('view').and_return(false) + @obj.has_view?('view') + end + + it "should proxy #view_by" do + Cat.should_receive(:view_by).with('name').and_return(false) + @obj.view_by('name') + end + + it "should proxy #view" do + Cat.should_receive(:view).with('view', {:database => 'database'}) + @obj.should_receive(:proxy_update_all) + @obj.view('view') + end + + it "should proxy #first_from_view" do + Cat.should_receive(:first_from_view).with('view', {:database => 'database'}) + @obj.should_receive(:proxy_update) + @obj.first_from_view('view') + end + + it "should proxy design_doc" do + Cat.should_receive(:design_doc) + @obj.design_doc + end + + it "should proxy refresh_design_doc" do + Cat.should_receive(:refresh_design_doc).with('database') + @obj.refresh_design_doc + end + + it "should proxy save_design_doc" do + Cat.should_receive(:save_design_doc).with('database') + @obj.save_design_doc + end + + ### Updating methods + + describe "#proxy_update" do + it "should set returned doc fields" do + doc = mock(:Document) + doc.should_receive(:respond_to?).with(:database=).and_return(true) + doc.should_receive(:database=).with('database') + doc.should_receive(:respond_to?).with(:model_proxy=).and_return(true) + doc.should_receive(:model_proxy=).with(@obj) + doc.should_receive(:respond_to?).with('owner_name=').and_return(true) + doc.should_receive(:send).with('owner_name=', 'owner') + @obj.send(:proxy_update, doc).should eql(doc) + end + + it "should not fail if some fields missing" do + doc = mock(:Document) + doc.should_receive(:respond_to?).with(:database=).and_return(true) + doc.should_receive(:database=).with('database') + doc.should_receive(:respond_to?).with(:model_proxy=).and_return(false) + doc.should_not_receive(:model_proxy=) + doc.should_receive(:respond_to?).with('owner_name=').and_return(false) + doc.should_not_receive(:owner_name=) + @obj.send(:proxy_update, doc).should eql(doc) + end + + it "should pass nil straight through without errors" do + lambda { @obj.send(:proxy_update, nil).should eql(nil) }.should_not raise_error + end + end + + it "#proxy_update_all should update array of docs" do + docs = [{}, {}] + @obj.should_receive(:proxy_update).twice.with({}) + @obj.send(:proxy_update_all, docs) + end + + end + + end + + describe "scenarios" do + + before :all do + class ProxyableCompany < CouchRest::Model::Base + use_database DB + property :slug + proxy_for :proxyable_invoices + def proxy_database + @db ||= TEST_SERVER.database!(TESTDB + "-#{slug}") + end + end + + class ProxyableInvoice < CouchRest::Model::Base + property :client + property :total + proxied_by :proxyable_company + validates_uniqueness_of :client + design do + view :by_total + end + end + + + @company = ProxyableCompany.create(:slug => 'samco') + end + + it "should create the new database" do + @company.proxyable_invoices.all.should be_empty + TEST_SERVER.databases.find{|db| db =~ /#{TESTDB}-samco/}.should_not be_nil + end + + it "should allow creation of new entries" do + inv = @company.proxyable_invoices.new(:client => "Lorena", :total => 35) + inv.save.should be_true + @company.proxyable_invoices.count.should eql(1) + @company.proxyable_invoices.first.client.should eql("Lorena") + end + + it "should validate uniqueness" do + inv = @company.proxyable_invoices.new(:client => "Lorena", :total => 40) + inv.save.should be_false + end + + it "should allow design views" do + item = @company.proxyable_invoices.by_total.key(35).first + item.client.should eql('Lorena') + end + + end + +end