diff --git a/lib/couchrest/model/designs.rb b/lib/couchrest/model/designs.rb index cd5e7d4..f025e42 100644 --- a/lib/couchrest/model/designs.rb +++ b/lib/couchrest/model/designs.rb @@ -23,8 +23,10 @@ module CouchRest def design(*args, &block) mapper = DesignMapper.new(self) - mapper.instance_eval(&block) + mapper.create_view_method(:all) + mapper.instance_eval(&block) + req_design_doc_refresh end @@ -43,6 +45,10 @@ module CouchRest # View instance when requested. def view(name, opts = {}) View.create(model, name, opts) + create_view_method(name) + end + + def create_view_method(name) model.class_eval <<-EOS, __FILE__, __LINE__ + 1 def self.#{name}(opts = {}) CouchRest::Model::Designs::View.new(self, opts, '#{name}') diff --git a/lib/couchrest/model/designs/view.rb b/lib/couchrest/model/designs/view.rb index da7c219..049cfb0 100644 --- a/lib/couchrest/model/designs/view.rb +++ b/lib/couchrest/model/designs/view.rb @@ -12,6 +12,7 @@ module CouchRest # a normal relational database are not possible. At least not yet! # class View + include Enumerable attr_accessor :model, :name, :query, :result @@ -20,7 +21,7 @@ module CouchRest if parent.is_a?(Class) && parent < CouchRest::Model::Base raise "Name must be provided for view to be initialized" if name.nil? self.model = parent - self.name = name + self.name = name.to_s # Default options: self.query = { :reduce => false } elsif parent.is_a?(self.class) @@ -37,33 +38,11 @@ module CouchRest # == View Execution Methods # - # Send a request to the CouchDB database using the current query values. - - # Inmediatly send a request to the database for all documents provided by the query. - # - def all(&block) - include_docs.rows.map{|r| r.doc} - end - - # Inmediatly send a request for the first result of the dataset. - # This will override any limit set in the view previously. - def first - limit(1).all.first - end - - - def info - - end - - def offset - execute['offset'] - end - - def total_rows - execute['total_rows'] - end - + # Request to the CouchDB database using the current query values. + + # Return each row wrapped in a ViewRow object. Unlike the raw + # CouchDB request, this will provide an empty array if there + # are no results. def rows return @rows if @rows if execute && result['rows'] @@ -73,10 +52,101 @@ module CouchRest end end + # Fetch all the documents the view can access. If the view has + # not already been prepared for including documents in the query, + # it will be added automatically and reset any previously cached + # results. + def all + include_docs! + docs + end + + # Provide all the documents from the view. If the view has not been + # prepared with the +include_docs+ option, each document will be + # loaded individually. + def docs + @docs ||= rows.map{|r| r.doc} + end + + # If another request has been made on the view, this will return + # the first document in the set. If not, a new query object will be + # generated with a limit of 1 so that only the first document is + # loaded. + def first + result ? all.first : limit(1).all.first + end + + # Same as first but will order the view in descending order. This + # does not however reverse the search keys or the offset, so if you + # are using a +startkey+ and +endkey+ you might end up with + # unexpected results. + # + # If in doubt, don't use this method! + # + def last + result ? all.last : limit(1).descending.all.last + end + + # Perform a count operation based on the current view. If the view + # can be reduced, the reduce will be performed and return the first + # value. This is okay for most simple queries, but may provide + # unexpected results if your reduce method does not calculate + # the total number of documents in a result set. + # + # Trying to use this method with the group option will raise an error. + # + # If no reduce function is defined, a query will be performed + # to return the total number of rows, this is the equivalant of: + # + # view.limit(0).total_rows + # + def count + raise "View#count cannot be used with group options" if query[:group] + if can_reduce? + row = reduce.rows.first + row.nil? ? 0 : row.value + else + limit(0).total_rows + end + 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. + def each(&block) + all.each(&block) + end + + # Wrapper for the results offset. As per the CouchDB API, + # this may be nil if groups are used. + def offset + execute['offset'] + end + + # Wrapper for the total_rows value provided by the query. As per the + # CouchDB API, this may be nil if groups are used. + def total_rows + execute['total_rows'] + end + + # Convenience wrapper around the rows result set. This will provide + # and array of keys. def keys rows.map{|r| r.key} end + # Convenience wrapper to provide all the values from the route + # set without having to go through +rows+. + def values + rows.map{|r| r.value} + end + + # No yet implemented. Eventually this will provide a raw hash + # of the information CouchDB holds about the view. + def info + raise "Not yet implemented" + end + # == View Filter Methods # @@ -85,6 +155,8 @@ module CouchRest # 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 @@ -97,11 +169,11 @@ module CouchRest update_query(:key => value) end - # Find all index keys that start with the value provided. May or may not be used in - # conjunction with the +endkey+ option. + # Find all index keys that start with the value provided. May or may + # not be used in conjunction with the +endkey+ option. # - # When the +#descending+ option is used (not the default), the start and end keys should - # be reversed. + # When the +#descending+ option is used (not the default), the start + # and end keys should be reversed, as per the CouchDB API. # # Cannot be used if the key has been set. def startkey(value) @@ -116,17 +188,19 @@ module CouchRest update_query(:startkey_docid => value.is_a?(String) ? value : value.id) end - # The opposite of +#startkey+, finds all index entries whose key is before the value specified. + # The opposite of +#startkey+, finds all index entries whose key is before + # the value specified. # - # See the +#startkey+ method for more details and the +#inclusive_end+ option. + # See the +#startkey+ method for more details and the +#inclusive_end+ + # option. def endkey(value) raise "View#endkey cannot be used when key has been set" unless query[:key].nil? update_query(:endkey => value) end # The result set should end at the position of the provided document. - # The value may be provided as an object that responds to the +#id+ call - # or a string. + # The value may be provided as an object that responds to the +#id+ + # call or a string. def endkey_doc(value) update_query(:endkey_docid => value.is_a?(String) ? value : value.id) end @@ -134,7 +208,8 @@ module CouchRest # The results should be provided in descending order. # - # Descending is false by default, this method will enable it and cannot be undone. + # Descending is false by default, this method will enable it and cannot + # be undone. def descending update_query(:descending => true) end @@ -156,54 +231,77 @@ module CouchRest # 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) end - # Control whether the reduce function reduces to a set of distinct keys or to a single - # result row. + # Control whether the reduce function reduces to a set of distinct keys + # or to a single result row. # - # By default the value is false, and can only be set when the view's +#reduce+ option - # has been set. + # By default the value is false, and can only be set when the view's + # +#reduce+ option has been set. def group raise "View#reduce must have been set before grouping is permitted" unless query[:reduce] update_query(:group => true) end + # Will set the level the grouping should be performed to. As per the + # CouchDB API, it only makes sense when the index key is an array. + # + # This will automatically set the group option. def group_level(value) - raise "View#reduce and View#group must have been set before group_level is called" unless query[:reduce] && query[:group] - update_query(:group_level => value.to_i) + group.update_query(:group_level => value.to_i) end + def include_docs + update_query.include_docs! + end + + # Return any cached values to their nil state so that any queries + # requested later will have a fresh set of data. + def reset! + self.result = nil + @rows = nil + @docs = nil + end protected + def include_docs! + reset! if result && !include_docs? + query[:include_docs] = true + self + end + + def include_docs? + !!query[:include_docs] + end + def update_query(new_query = {}) self.class.new(self, new_query) end - def database - query[:database] || model.database + def design_doc + model.design_doc end - # Used internally to ensure that docs are provided. Should not be used outside of - # the view class under normal circumstances. - def include_docs - raise "Documents cannot be returned from a view that is prepared for a reduce" if query[:reduce] - query.delete(:reduce) - update_query(:include_docs => true) + def can_reduce? + !design_doc['views'][name]['reduce'].blank? end - def execute(&block) + + def execute return self.result if result - raise "Database must be defined in model or view!" if database.nil? + db = query[:database] || model.database + raise "Database must be defined in model or view!" if db.nil? retryable = true # Remove the reduce value if its not needed - query.delete(:reduce) if !query[:reduce] && model.design_doc['views'][name.to_s]['reduce'].blank? + query.delete(:reduce) unless can_reduce? begin - self.result = model.design_doc.view_on(database, name, query, &block) + self.result = model.design_doc.view_on(db, name, query) rescue RestClient::ResourceNotFound => e if retryable - model.save_design_doc(database) + model.save_design_doc(db) retryable = false retry else @@ -248,19 +346,24 @@ module CouchRest emit = keys.length == 1 ? keys.first : "[#{keys.join(', ')}]" opts[:guards] += keys.map{|k| "(#{k} != null)"} opts[:map] = <<-EOF -function(doc) { - if (#{opts[:guards].join(' && ')}) { - emit(#{emit}, null); - } -} -EOF + function(doc) { + if (#{opts[:guards].join(' && ')}) { + emit(#{emit}, 1); + } + } + EOF + opts[:reduce] = <<-EOF + function(key, values, rereduce) { + return sum(values); + } + EOF end model.design_doc['views'] ||= {} - model.design_doc['views'][name.to_s] = { - 'map' => opts[:map], - 'reduce' => opts[:reduce] - } + view = model.design_doc['views'][name.to_s] = { } + view['map'] = opts[:map] + view['reduce'] = opts[:reduce] if opts[:reduce] + view end end diff --git a/spec/couchrest/designs/view_spec.rb b/spec/couchrest/designs/view_spec.rb index c83fd5b..7670f45 100644 --- a/spec/couchrest/designs/view_spec.rb +++ b/spec/couchrest/designs/view_spec.rb @@ -12,116 +12,553 @@ end describe "Design View" do - before :each do - @klass = CouchRest::Model::Designs::View - end + describe "(unit tests)" do - describe ".new" do - - describe "with invalid parent model" do - it "should burn" do - lambda { @klass.new(String) }.should raise_exception - end + before :each do + @klass = CouchRest::Model::Designs::View end - describe "with CouchRest Model" do + describe ".new" do - it "should setup attributes" do - @obj = @klass.new(DesignViewModel, {}, 'test_view') - @obj.model.should eql(DesignViewModel) - @obj.name.should eql('test_view') - @obj.query.should eql({:reduce => false}) + describe "with invalid parent model" do + it "should burn" do + lambda { @klass.new(String) }.should raise_exception + end end - it "should complain if there is no name" do - lambda { @klass.new(DesignViewModel, {}, nil) }.should raise_error + describe "with CouchRest Model" do + + it "should setup attributes" do + @obj = @klass.new(DesignViewModel, {}, 'test_view') + @obj.model.should eql(DesignViewModel) + @obj.name.should eql('test_view') + @obj.query.should eql({:reduce => false}) + end + + it "should complain if there is no name" do + lambda { @klass.new(DesignViewModel, {}, nil) }.should raise_error + end + + end + + describe "with previous view instance" do + + before :each do + first = @klass.new(DesignViewModel, {}, 'test_view') + @obj = @klass.new(first, {:foo => :bar}) + end + + it "should copy attributes" do + @obj.model.should eql(DesignViewModel) + @obj.name.should eql('test_view') + @obj.query.should eql({:reduce => false, :foo => :bar}) + end + end end - describe "with previous view instance" do + describe ".create" do before :each do - first = @klass.new(DesignViewModel, {}, 'test_view') - @obj = @klass.new(first, {:foo => :bar}) + @design_doc = {} + DesignViewModel.stub!(:design_doc).and_return(@design_doc) end - it "should copy attributes" do - @obj.model.should eql(DesignViewModel) - @obj.name.should eql('test_view') - @obj.query.should eql({:reduce => false, :foo => :bar}) + it "should add a basic view" do + @klass.create(DesignViewModel, 'test_view', :map => 'foo') + @design_doc['views']['test_view'].should_not be_nil + end + + it "should auto generate mapping from name" do + lambda { @klass.create(DesignViewModel, 'by_title') }.should_not raise_error + str = @design_doc['views']['by_title']['map'] + str.should include("((doc['couchrest-type'] == 'DesignViewModel') && (doc['title'] != null))") + str.should include("emit(doc['title'], 1);") + str = @design_doc['views']['by_title']['reduce'] + str.should include("return sum(values);") + end + + it "should auto generate mapping from name with and" do + @klass.create(DesignViewModel, 'by_title_and_name') + str = @design_doc['views']['by_title_and_name']['map'] + str.should include("(doc['title'] != null) && (doc['name'] != null)") + str.should include("emit([doc['title'], doc['name']], 1);") + str = @design_doc['views']['by_title_and_name']['reduce'] + str.should include("return sum(values);") end end - end + describe "instance methods" do - describe ".create" do - - before :each do - @design_doc = {} - DesignViewModel.stub!(:design_doc).and_return(@design_doc) - end - - it "should add a basic view" do - @klass.create(DesignViewModel, 'test_view', :map => 'foo') - @design_doc['views']['test_view'].should_not be_nil - end - - it "should auto generate mapping from name" do - lambda { @klass.create(DesignViewModel, 'by_title') }.should_not raise_error - str = @design_doc['views']['by_title']['map'] - str.should include("((doc['couchrest-type'] == 'DesignViewModel') && (doc['title'] != null))") - str.should include("emit(doc['title'], null);") - end - - it "should auto generate mapping from name with and" do - @klass.create(DesignViewModel, 'by_title_and_name') - str = @design_doc['views']['by_title_and_name']['map'] - str.should include("(doc['title'] != null) && (doc['name'] != null)") - str.should include("emit([doc['title'], doc['name']], null);") - end - - end - - describe "instance methods" do - - before :each do - @obj = @klass.new(DesignViewModel, {}, 'test_view') - end - - describe "#update_query" do - it "returns a new instance of view" do - @obj.send(:update_query).object_id.should_not eql(@obj.object_id) + before :each do + @obj = @klass.new(DesignViewModel, {}, 'test_view') end - it "returns a new instance of view with extra parameters" do - new_obj = @obj.send(:update_query, {:foo => :bar}) - new_obj.query[:foo].should eql(:bar) + describe "#rows" do + it "should execute query" do + @obj.should_receive(:execute).and_return(true) + @obj.should_receive(:result).twice.and_return({'rows' => []}) + @obj.rows.should be_empty + end + + it "should wrap rows in ViewRow class" do + @obj.should_receive(:execute).and_return(true) + @obj.should_receive(:result).twice.and_return({'rows' => [{:foo => :bar}]}) + CouchRest::Model::Designs::ViewRow.should_receive(:new).with({:foo => :bar}, @obj.model) + @obj.rows + end + end + + describe "#all" do + it "should ensure docs included and call docs" do + @obj.should_receive(:include_docs!) + @obj.should_receive(:docs) + @obj.all + end + end + + describe "#docs" do + it "should provide docs from rows" do + @obj.should_receive(:rows).and_return([]) + @obj.docs + end + it "should cache the results" do + @obj.should_receive(:rows).once.and_return([]) + @obj.docs + @obj.docs + end + end + + describe "#first" do + it "should provide the first result of loaded query" do + @obj.should_receive(:result).and_return(true) + @obj.should_receive(:all).and_return([:foo]) + @obj.first.should eql(:foo) + end + it "should perform a query if no results cached" do + view = mock('SubView') + @obj.should_receive(:result).and_return(nil) + @obj.should_receive(:limit).with(1).and_return(view) + view.should_receive(:all).and_return([:foo]) + @obj.first.should eql(:foo) + end + end + + describe "#last" do + it "should provide the last result of loaded query" do + @obj.should_receive(:result).and_return(true) + @obj.should_receive(:all).and_return([:foo, :bar]) + @obj.first.should eql(:foo) + end + it "should perform a query if no results cached" do + view = mock('SubView') + @obj.should_receive(:result).and_return(nil) + @obj.should_receive(:limit).with(1).and_return(view) + view.should_receive(:descending).and_return(view) + view.should_receive(:all).and_return([:foo, :bar]) + @obj.last.should eql(:bar) + end + end + + describe "#count" do + it "should raise an error if view prepared for group" do + @obj.should_receive(:query).and_return({:group => true}) + lambda { @obj.count }.should raise_error + end + + it "should return first row value if reduce possible" do + view = mock("SubView") + row = mock("Row") + @obj.should_receive(:can_reduce?).and_return(true) + @obj.should_receive(:reduce).and_return(view) + view.should_receive(:rows).and_return([row]) + row.should_receive(:value).and_return(2) + @obj.count.should eql(2) + end + it "should return 0 if no rows and reduce possible" do + view = mock("SubView") + @obj.should_receive(:can_reduce?).and_return(true) + @obj.should_receive(:reduce).and_return(view) + view.should_receive(:rows).and_return([]) + @obj.count.should eql(0) + end + + it "should perform limit request for total_rows" do + view = mock("SubView") + @obj.should_receive(:limit).with(0).and_return(view) + view.should_receive(:total_rows).and_return(4) + @obj.should_receive(:can_reduce?).and_return(false) + @obj.count.should eql(4) + end + end + + describe "#each" do + it "should call each method on all" do + @obj.should_receive(:all).and_return([]) + @obj.each + end + it "should call each and pass block" do + set = [:foo, :bar] + @obj.should_receive(:all).and_return(set) + result = [] + @obj.each do |s| + result << s + end + result.should eql(set) + end + end + + describe "#offset" do + it "should excute" do + @obj.should_receive(:execute).and_return({'offset' => 3}) + @obj.offset.should eql(3) + end + end + + describe "#total_rows" do + it "should excute" do + @obj.should_receive(:execute).and_return({'total_rows' => 3}) + @obj.total_rows.should eql(3) + end + end + + describe "#keys" do + it "should request each row and provide key value" do + row = mock("Row") + row.should_receive(:key).twice.and_return('foo') + @obj.should_receive(:rows).and_return([row, row]) + @obj.keys.should eql(['foo', 'foo']) + end + end + + describe "#values" do + it "should request each row and provide value" do + row = mock("Row") + row.should_receive(:value).twice.and_return('foo') + @obj.should_receive(:rows).and_return([row, row]) + @obj.values.should eql(['foo', 'foo']) + end + end + + describe "#info" do + it "should raise error" do + lambda { @obj.info }.should raise_error + end + end + + describe "#database" do + it "should update query with value" do + @obj.should_receive(:update_query).with({:database => 'foo'}) + @obj.database('foo') + end + end + + describe "#key" do + it "should update query with value" do + @obj.should_receive(:update_query).with({:key => 'foo'}) + @obj.key('foo') + end + it "should raise and error if startkey set" do + @obj.query[:startkey] = 'bar' + lambda { @obj.key('foo') }.should raise_error + end + it "should raise and error if endkey set" do + @obj.query[:endkey] = 'bar' + lambda { @obj.key('foo') }.should raise_error + end + it "should raise and error if both startkey and endkey set" do + @obj.query[:startkey] = 'bar' + @obj.query[:endkey] = 'bar' + lambda { @obj.key('foo') }.should raise_error + end + end + + describe "#startkey" do + it "should update query with value" do + @obj.should_receive(:update_query).with({:startkey => 'foo'}) + @obj.startkey('foo') + end + it "should raise and error if key set" do + @obj.query[:key] = 'bar' + lambda { @obj.startkey('foo') }.should raise_error + end + end + + describe "#startkey_doc" do + it "should update query with value" do + @obj.should_receive(:update_query).with({:startkey_docid => 'foo'}) + @obj.startkey_doc('foo') + end + it "should update query with object id if available" do + doc = mock("Document") + doc.should_receive(:id).and_return(44) + @obj.should_receive(:update_query).with({:startkey_docid => 44}) + @obj.startkey_doc(doc) + end + end + + describe "#endkey" do + it "should update query with value" do + @obj.should_receive(:update_query).with({:endkey => 'foo'}) + @obj.endkey('foo') + end + it "should raise and error if key set" do + @obj.query[:key] = 'bar' + lambda { @obj.endkey('foo') }.should raise_error + end + end + + describe "#endkey_doc" do + it "should update query with value" do + @obj.should_receive(:update_query).with({:endkey_docid => 'foo'}) + @obj.endkey_doc('foo') + end + it "should update query with object id if available" do + doc = mock("Document") + doc.should_receive(:id).and_return(44) + @obj.should_receive(:update_query).with({:endkey_docid => 44}) + @obj.endkey_doc(doc) + end + end + + describe "#descending" do + it "should update query" do + @obj.should_receive(:update_query).with({:descending => true}) + @obj.descending + end + end + + describe "#limit" do + it "should update query with value" do + @obj.should_receive(:update_query).with({:limit => 3}) + @obj.limit(3) + end + end + + describe "#skip" do + it "should update query with value" do + @obj.should_receive(:update_query).with({:skip => 3}) + @obj.skip(3) + end + it "should update query with default value" do + @obj.should_receive(:update_query).with({:skip => 0}) + @obj.skip + end + end + + describe "#reduce" do + it "should update query" do + @obj.should_receive(:can_reduce?).and_return(true) + @obj.should_receive(:update_query).with({:reduce => true}) + @obj.reduce + end + it "should raise error if query cannot be reduced" do + @obj.should_receive(:can_reduce?).and_return(false) + lambda { @obj.reduce }.should raise_error + end + end + + describe "#group" do + it "should update query" do + @obj.should_receive(:query).and_return({:reduce => true}) + @obj.should_receive(:update_query).with({:group => true}) + @obj.group + end + it "should raise error if query not prepared for reduce" do + @obj.should_receive(:query).and_return({:reduce => false}) + lambda { @obj.group }.should raise_error + end + end + + describe "#group" do + it "should update query" do + @obj.should_receive(:query).and_return({:reduce => true}) + @obj.should_receive(:update_query).with({:group => true}) + @obj.group + end + it "should raise error if query not prepared for reduce" do + @obj.should_receive(:query).and_return({:reduce => false}) + lambda { @obj.group }.should raise_error + end + end + + describe "#group_level" do + it "should update query" do + @obj.should_receive(:group).and_return(@obj) + @obj.should_receive(:update_query).with({:group_level => 3}) + @obj.group_level(3) + end + end + + describe "#include_docs" do + it "should call include_docs! on new view" do + @obj.should_receive(:update_query).and_return(@obj) + @obj.should_receive(:include_docs!) + @obj.include_docs + end + end + + describe "#reset!" do + it "should empty all cached data" do + @obj.should_receive(:result=).with(nil) + @obj.instance_exec { @rows = 'foo'; @docs = 'foo' } + @obj.reset! + @obj.instance_exec { @rows }.should be_nil + @obj.instance_exec { @docs }.should be_nil + end + end + + #### PROTECTED METHODS + + describe "#include_docs!" do + it "should set query value" do + @obj.should_receive(:result).and_return(false) + @obj.should_not_receive(:reset!) + @obj.send(:include_docs!) + @obj.query[:include_docs].should be_true + end + it "should reset if result and no docs" do + @obj.should_receive(:result).and_return(true) + @obj.should_receive(:include_docs?).and_return(false) + @obj.should_receive(:reset!) + @obj.send(:include_docs!) + @obj.query[:include_docs].should be_true + end + end + + describe "#include_docs?" do + it "should return true if set" do + @obj.should_receive(:query).and_return({:include_docs => true}) + @obj.send(:include_docs?).should be_true + end + it "should return false if not set" do + @obj.should_receive(:query).and_return({}) + @obj.send(:include_docs?).should be_false + @obj.should_receive(:query).and_return({:include_docs => false}) + @obj.send(:include_docs?).should be_false + end + end + + describe "#update_query" do + it "returns a new instance of view" do + @obj.send(:update_query).object_id.should_not eql(@obj.object_id) + end + + it "returns a new instance of view with extra parameters" do + new_obj = @obj.send(:update_query, {:foo => :bar}) + new_obj.query[:foo].should eql(:bar) + end + end + + describe "#design_doc" do + it "should call design_doc on model" do + @obj.model.should_receive(:design_doc) + @obj.send(:design_doc) + end + end + + describe "#can_reduce?" do + it "should check and prove true" do + @obj.should_receive(:name).and_return('test_view') + @obj.should_receive(:design_doc).and_return({'views' => {'test_view' => {'reduce' => 'foo'}}}) + @obj.send(:can_reduce?).should be_true + end + it "should check and prove false" do + @obj.should_receive(:name).and_return('test_view') + @obj.should_receive(:design_doc).and_return({'views' => {'test_view' => {'reduce' => nil}}}) + @obj.send(:can_reduce?).should be_false + end + end + + describe "#execute" do + before :each do + # disable real execution! + @design_doc = mock("DesignDoc") + @design_doc.stub!(:view_on) + @obj.model.stub!(:design_doc).and_return(@design_doc) + end + + it "should return previous result if set" do + @obj.result = "foo" + @obj.send(:execute).should eql('foo') + end + + it "should raise issue if no database" do + @obj.should_receive(:query).and_return({:database => nil}) + model = mock("SomeModel") + model.should_receive(:database).and_return(nil) + @obj.should_receive(:model).and_return(model) + lambda { @obj.send(:execute) }.should raise_error + end + + it "should delete the reduce option if not going to be used" do + @obj.should_receive(:can_reduce?).and_return(false) + @obj.query.should_receive(:delete).with(:reduce) + @obj.send(:execute) + end + + it "should populate the results" do + @obj.should_receive(:can_reduce?).and_return(true) + @design_doc.should_receive(:view_on).and_return('foos') + @obj.send(:execute) + @obj.result.should eql('foos') + end + + 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') + @obj.send(:execute) + @obj.result.should eql('foos') + end + + 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) + lambda { @obj.send(:execute) }.should raise_error(RestClient::ResourceNotFound) + end + + end end - end - ############## - describe "with real data" do + describe "scenarios" do before :all do @objs = [ - {:name => "Sam"}, + {:name => "Judith"}, {:name => "Lorena"}, {:name => "Peter"}, - {:name => "Judith"}, + {:name => "Sam"}, {:name => "Vilma"} ].map{|h| DesignViewModel.create(h)} end - describe "just documents" do + describe "loading documents" do - it "should return all" do - DesignViewModel.by_name.all.last.name.should eql("Vilma") + it "should return first" do + DesignViewModel.by_name.first.name.should eql("Judith") + end + + it "should return last" do + DesignViewModel.by_name.last.name.should eql("Vilma") + end + + it "should allow multiple results" do + view = DesignViewModel.by_name.limit(3) + view.total_rows.should eql(5) + view.last.name.should eql("Peter") + view.all.length.should eql(3) end end @@ -139,7 +576,6 @@ describe "Design View" do it "should provide a set of keys" do DesignViewModel.by_name.limit(2).keys.should eql(["Judith", "Lorena"]) end - end end diff --git a/spec/couchrest/designs_spec.rb b/spec/couchrest/designs_spec.rb index cee7665..8897766 100644 --- a/spec/couchrest/designs_spec.rb +++ b/spec/couchrest/designs_spec.rb @@ -11,21 +11,22 @@ describe "Design" do end describe ".design" do - - it "should instantiate a new DesignMapper" do - CouchRest::Model::Designs::DesignMapper.should_receive(:new).and_return(DesignModel) - DesignModel.design() { } + + before :each do + @mapper = mock('DesignMapper') + @mapper.stub!(:create_view_method) end - it "should instantiate a new DesignMapper with model" do - CouchRest::Model::Designs::DesignMapper.should_receive(:new).with(DesignModel).and_return(DesignModel) + it "should instantiate a new DesignMapper" do + CouchRest::Model::Designs::DesignMapper.should_receive(:new).with(DesignModel).and_return(@mapper) + @mapper.should_receive(:create_view_method).with(:all) + @mapper.should_receive(:instance_eval) DesignModel.design() { } end it "should allow methods to be called in mapper" do - model = mock('Foo') - model.should_receive(:foo) - CouchRest::Model::Designs::DesignMapper.stub!(:new).and_return(model) + @mapper.should_receive(:foo) + CouchRest::Model::Designs::DesignMapper.stub!(:new).and_return(@mapper) DesignModel.design { foo } end @@ -64,10 +65,22 @@ describe "Design" do DesignModel.should respond_to(:test_view) end - it "should create a method that returns view instance" do + it "should create a method for view instance" do CouchRest::Model::Designs::View.stub!(:create) - @object.view('test_view') + @object.should_receive(:create_view_method).with('test') + @object.view('test') + end + + end + + describe "#create_view_method" do + before :each do + @object = @klass.new(DesignModel) + end + + it "should create a method that returns view instance" do CouchRest::Model::Designs::View.should_receive(:new).with(DesignModel, {}, 'test_view').and_return(nil) + @object.create_view_method('test_view') DesignModel.test_view end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 415a2c7..3a865b5 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -32,7 +32,7 @@ RSpec.configure do |config| cr = TEST_SERVER test_dbs = cr.databases.select { |db| db =~ /^#{TESTDB}/ } test_dbs.each do |db| - #cr.database(db).delete! rescue nil + cr.database(db).delete! rescue nil end end end