Finalizing structure and tests for new basic design view support

This commit is contained in:
Sam Lown 2011-02-06 17:17:14 +01:00
parent 800c2b322c
commit 4d1aebec43
5 changed files with 711 additions and 153 deletions

View file

@ -23,6 +23,8 @@ module CouchRest
def design(*args, &block) def design(*args, &block)
mapper = DesignMapper.new(self) mapper = DesignMapper.new(self)
mapper.create_view_method(:all)
mapper.instance_eval(&block) mapper.instance_eval(&block)
req_design_doc_refresh req_design_doc_refresh
@ -43,6 +45,10 @@ module CouchRest
# View instance when requested. # View instance when requested.
def view(name, opts = {}) def view(name, opts = {})
View.create(model, name, opts) View.create(model, name, opts)
create_view_method(name)
end
def create_view_method(name)
model.class_eval <<-EOS, __FILE__, __LINE__ + 1 model.class_eval <<-EOS, __FILE__, __LINE__ + 1
def self.#{name}(opts = {}) def self.#{name}(opts = {})
CouchRest::Model::Designs::View.new(self, opts, '#{name}') CouchRest::Model::Designs::View.new(self, opts, '#{name}')

View file

@ -12,6 +12,7 @@ module CouchRest
# a normal relational database are not possible. At least not yet! # a normal relational database are not possible. At least not yet!
# #
class View class View
include Enumerable
attr_accessor :model, :name, :query, :result attr_accessor :model, :name, :query, :result
@ -20,7 +21,7 @@ module CouchRest
if parent.is_a?(Class) && parent < CouchRest::Model::Base if parent.is_a?(Class) && parent < CouchRest::Model::Base
raise "Name must be provided for view to be initialized" if name.nil? raise "Name must be provided for view to be initialized" if name.nil?
self.model = parent self.model = parent
self.name = name self.name = name.to_s
# Default options: # Default options:
self.query = { :reduce => false } self.query = { :reduce => false }
elsif parent.is_a?(self.class) elsif parent.is_a?(self.class)
@ -37,33 +38,11 @@ module CouchRest
# == View Execution Methods # == View Execution Methods
# #
# Send a request to the CouchDB database using the current query values. # 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
# 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 def rows
return @rows if @rows return @rows if @rows
if execute && result['rows'] if execute && result['rows']
@ -73,10 +52,101 @@ module CouchRest
end end
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 def keys
rows.map{|r| r.key} rows.map{|r| r.key}
end 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 # == View Filter Methods
# #
@ -85,6 +155,8 @@ module CouchRest
# are combined in an incorrect fashion. # 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) def database(value)
update_query(:database => value) update_query(:database => value)
end end
@ -97,11 +169,11 @@ module CouchRest
update_query(:key => value) update_query(:key => value)
end end
# Find all index keys that start with the value provided. May or may not be used in # Find all index keys that start with the value provided. May or may
# conjunction with the +endkey+ option. # not be used in conjunction with the +endkey+ option.
# #
# When the +#descending+ option is used (not the default), the start and end keys should # When the +#descending+ option is used (not the default), the start
# be reversed. # and end keys should be reversed, as per the CouchDB API.
# #
# Cannot be used if the key has been set. # Cannot be used if the key has been set.
def startkey(value) def startkey(value)
@ -116,17 +188,19 @@ module CouchRest
update_query(:startkey_docid => value.is_a?(String) ? value : value.id) update_query(:startkey_docid => value.is_a?(String) ? value : value.id)
end 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) def endkey(value)
raise "View#endkey cannot be used when key has been set" unless query[:key].nil? raise "View#endkey cannot be used when key has been set" unless query[:key].nil?
update_query(:endkey => value) update_query(:endkey => value)
end end
# The result set should end at the position of the provided document. # 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 # The value may be provided as an object that responds to the +#id+
# or a string. # call or a string.
def endkey_doc(value) def endkey_doc(value)
update_query(:endkey_docid => value.is_a?(String) ? value : value.id) update_query(:endkey_docid => value.is_a?(String) ? value : value.id)
end end
@ -134,7 +208,8 @@ module CouchRest
# The results should be provided in descending order. # 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 def descending
update_query(:descending => true) update_query(:descending => true)
end end
@ -156,54 +231,77 @@ module CouchRest
# 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 def reduce
raise "Cannot reduce a view without a reduce method" unless can_reduce?
update_query(:reduce => true) update_query(:reduce => true)
end end
# Control whether the reduce function reduces to a set of distinct keys or to a single # Control whether the reduce function reduces to a set of distinct keys
# result row. # or to a single result row.
# #
# By default the value is false, and can only be set when the view's +#reduce+ option # By default the value is false, and can only be set when the view's
# has been set. # +#reduce+ option has been set.
def group def group
raise "View#reduce must have been set before grouping is permitted" unless query[:reduce] raise "View#reduce must have been set before grouping is permitted" unless query[:reduce]
update_query(:group => true) update_query(:group => true)
end 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) def group_level(value)
raise "View#reduce and View#group must have been set before group_level is called" unless query[:reduce] && query[:group] group.update_query(:group_level => value.to_i)
update_query(:group_level => value.to_i)
end 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 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 = {}) def update_query(new_query = {})
self.class.new(self, new_query) self.class.new(self, new_query)
end end
def database def design_doc
query[:database] || model.database model.design_doc
end end
# Used internally to ensure that docs are provided. Should not be used outside of def can_reduce?
# the view class under normal circumstances. !design_doc['views'][name]['reduce'].blank?
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)
end end
def execute(&block)
def execute
return self.result if result 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 retryable = true
# Remove the reduce value if its not needed # 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 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 rescue RestClient::ResourceNotFound => e
if retryable if retryable
model.save_design_doc(database) model.save_design_doc(db)
retryable = false retryable = false
retry retry
else else
@ -248,19 +346,24 @@ module CouchRest
emit = keys.length == 1 ? keys.first : "[#{keys.join(', ')}]" emit = keys.length == 1 ? keys.first : "[#{keys.join(', ')}]"
opts[:guards] += keys.map{|k| "(#{k} != null)"} opts[:guards] += keys.map{|k| "(#{k} != null)"}
opts[:map] = <<-EOF opts[:map] = <<-EOF
function(doc) { function(doc) {
if (#{opts[:guards].join(' && ')}) { if (#{opts[:guards].join(' && ')}) {
emit(#{emit}, null); emit(#{emit}, 1);
} }
} }
EOF EOF
opts[:reduce] = <<-EOF
function(key, values, rereduce) {
return sum(values);
}
EOF
end end
model.design_doc['views'] ||= {} model.design_doc['views'] ||= {}
model.design_doc['views'][name.to_s] = { view = model.design_doc['views'][name.to_s] = { }
'map' => opts[:map], view['map'] = opts[:map]
'reduce' => opts[:reduce] view['reduce'] = opts[:reduce] if opts[:reduce]
} view
end end
end end

View file

@ -12,116 +12,553 @@ end
describe "Design View" do describe "Design View" do
before :each do describe "(unit tests)" do
@klass = CouchRest::Model::Designs::View
end
describe ".new" do before :each do
@klass = CouchRest::Model::Designs::View
describe "with invalid parent model" do
it "should burn" do
lambda { @klass.new(String) }.should raise_exception
end
end end
describe "with CouchRest Model" do describe ".new" do
it "should setup attributes" do describe "with invalid parent model" do
@obj = @klass.new(DesignViewModel, {}, 'test_view') it "should burn" do
@obj.model.should eql(DesignViewModel) lambda { @klass.new(String) }.should raise_exception
@obj.name.should eql('test_view') end
@obj.query.should eql({:reduce => false})
end end
it "should complain if there is no name" do describe "with CouchRest Model" do
lambda { @klass.new(DesignViewModel, {}, nil) }.should raise_error
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
end end
describe "with previous view instance" do describe ".create" do
before :each do before :each do
first = @klass.new(DesignViewModel, {}, 'test_view') @design_doc = {}
@obj = @klass.new(first, {:foo => :bar}) DesignViewModel.stub!(:design_doc).and_return(@design_doc)
end end
it "should copy attributes" do it "should add a basic view" do
@obj.model.should eql(DesignViewModel) @klass.create(DesignViewModel, 'test_view', :map => 'foo')
@obj.name.should eql('test_view') @design_doc['views']['test_view'].should_not be_nil
@obj.query.should eql({:reduce => false, :foo => :bar}) 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 end
describe "instance methods" do
before :each do
@obj = @klass.new(DesignViewModel, {}, 'test_view')
end
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 end
describe ".create" do
before :each do describe "scenarios" 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)
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
end
##############
describe "with real data" do
before :all do before :all do
@objs = [ @objs = [
{:name => "Sam"}, {:name => "Judith"},
{:name => "Lorena"}, {:name => "Lorena"},
{:name => "Peter"}, {:name => "Peter"},
{:name => "Judith"}, {:name => "Sam"},
{:name => "Vilma"} {:name => "Vilma"}
].map{|h| DesignViewModel.create(h)} ].map{|h| DesignViewModel.create(h)}
end end
describe "just documents" do describe "loading documents" do
it "should return all" do it "should return first" do
DesignViewModel.by_name.all.last.name.should eql("Vilma") 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
end end
@ -139,7 +576,6 @@ describe "Design View" do
it "should provide a set of keys" do it "should provide a set of keys" do
DesignViewModel.by_name.limit(2).keys.should eql(["Judith", "Lorena"]) DesignViewModel.by_name.limit(2).keys.should eql(["Judith", "Lorena"])
end end
end end
end end

View file

@ -12,20 +12,21 @@ describe "Design" do
describe ".design" do describe ".design" do
it "should instantiate a new DesignMapper" do before :each do
CouchRest::Model::Designs::DesignMapper.should_receive(:new).and_return(DesignModel) @mapper = mock('DesignMapper')
DesignModel.design() { } @mapper.stub!(:create_view_method)
end end
it "should instantiate a new DesignMapper with model" do it "should instantiate a new DesignMapper" do
CouchRest::Model::Designs::DesignMapper.should_receive(:new).with(DesignModel).and_return(DesignModel) 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() { } DesignModel.design() { }
end end
it "should allow methods to be called in mapper" do it "should allow methods to be called in mapper" do
model = mock('Foo') @mapper.should_receive(:foo)
model.should_receive(:foo) CouchRest::Model::Designs::DesignMapper.stub!(:new).and_return(@mapper)
CouchRest::Model::Designs::DesignMapper.stub!(:new).and_return(model)
DesignModel.design { foo } DesignModel.design { foo }
end end
@ -64,10 +65,22 @@ describe "Design" do
DesignModel.should respond_to(:test_view) DesignModel.should respond_to(:test_view)
end 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) 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) CouchRest::Model::Designs::View.should_receive(:new).with(DesignModel, {}, 'test_view').and_return(nil)
@object.create_view_method('test_view')
DesignModel.test_view DesignModel.test_view
end end

View file

@ -32,7 +32,7 @@ RSpec.configure do |config|
cr = TEST_SERVER cr = TEST_SERVER
test_dbs = cr.databases.select { |db| db =~ /^#{TESTDB}/ } test_dbs = cr.databases.select { |db| db =~ /^#{TESTDB}/ }
test_dbs.each do |db| test_dbs.each do |db|
#cr.database(db).delete! rescue nil cr.database(db).delete! rescue nil
end end
end end
end end