Working on testing for design and view support
This commit is contained in:
parent
a79c2d516a
commit
dc28155aa3
|
@ -16,6 +16,7 @@ module CouchRest
|
||||||
include CouchRest::Model::PropertyProtection
|
include CouchRest::Model::PropertyProtection
|
||||||
include CouchRest::Model::Associations
|
include CouchRest::Model::Associations
|
||||||
include CouchRest::Model::Validations
|
include CouchRest::Model::Validations
|
||||||
|
include CouchRest::Model::Design
|
||||||
|
|
||||||
def self.subclasses
|
def self.subclasses
|
||||||
@subclasses ||= []
|
@subclasses ||= []
|
||||||
|
|
|
@ -22,18 +22,32 @@ module CouchRest
|
||||||
module ClassMethods
|
module ClassMethods
|
||||||
|
|
||||||
def design(*args, &block)
|
def design(*args, &block)
|
||||||
|
mapper = DesignMapper.new(self)
|
||||||
|
mapper.instance_eval(&block)
|
||||||
|
|
||||||
|
req_design_doc_refresh
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
#
|
#
|
||||||
module DesignMethods
|
class DesignMapper
|
||||||
|
|
||||||
|
attr_accessor :model
|
||||||
|
|
||||||
def view(*args)
|
def initialize(model)
|
||||||
|
self.model = model
|
||||||
|
end
|
||||||
|
|
||||||
|
# Define a view and generate a method that will provide a new
|
||||||
|
# View instance when requested.
|
||||||
|
def view(name, opts = {})
|
||||||
|
View.create(model, name, opts)
|
||||||
|
model.class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||||
|
def self.#{name}(opts = {})
|
||||||
|
CouchRest::Model::Design::View.new(self, opts, '#{name}')
|
||||||
|
end
|
||||||
|
EOS
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -2,8 +2,6 @@ module CouchRest
|
||||||
module Model
|
module Model
|
||||||
module Design
|
module Design
|
||||||
|
|
||||||
#
|
|
||||||
# ### NOTE Work in progress! Not yet used! ###
|
|
||||||
#
|
#
|
||||||
# A proxy class that allows view queries to be created using
|
# A proxy class that allows view queries to be created using
|
||||||
# chained method calls. After each call a new instance of the method
|
# chained method calls. After each call a new instance of the method
|
||||||
|
@ -13,26 +11,26 @@ module CouchRest
|
||||||
# CouchDB views have inherent limitations, so joins and filters as used in
|
# 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. At least not yet!
|
||||||
#
|
#
|
||||||
#
|
|
||||||
#
|
|
||||||
class View
|
class View
|
||||||
|
|
||||||
attr_accessor :query, :design, :database, :name
|
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)
|
def initialize(parent, new_query = {}, name = nil)
|
||||||
if parent.is_a? 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?
|
||||||
@name = name
|
self.model = parent
|
||||||
@database = parent.database
|
self.name = name
|
||||||
@query = { :reduce => false }
|
# Default options:
|
||||||
elsif parent.is_a? View
|
self.query = { :reduce => false }
|
||||||
@database = parent.database
|
elsif parent.is_a?(self.class)
|
||||||
@query = parent.query.dup
|
self.model = parent.model
|
||||||
|
self.name = parent.name
|
||||||
|
self.query = parent.query.dup
|
||||||
else
|
else
|
||||||
raise "View cannot be initialized without a parent Model or View"
|
raise "View cannot be initialized without a parent Model or View"
|
||||||
end
|
end
|
||||||
@query.update(new_query)
|
query.update(new_query)
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
|
@ -44,15 +42,13 @@ module CouchRest
|
||||||
# Inmediatly send a request to the database for all documents provided by the query.
|
# Inmediatly send a request to the database for all documents provided by the query.
|
||||||
#
|
#
|
||||||
def all(&block)
|
def all(&block)
|
||||||
args = include_docs.query
|
include_docs.execute(&block)
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# Inmediatly send a request for the first result of the dataset. This will override
|
# Inmediatly send a request for the first result of the dataset. This will override
|
||||||
# any limit set in the view previously.
|
# any limit set in the view previously.
|
||||||
def first(&block)
|
def first
|
||||||
args = limit(1).include_docs.query
|
limit(1).include_docs.execute.first
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def info
|
def info
|
||||||
|
@ -60,15 +56,19 @@ module CouchRest
|
||||||
end
|
end
|
||||||
|
|
||||||
def offset
|
def offset
|
||||||
|
execute['offset']
|
||||||
end
|
end
|
||||||
|
|
||||||
def total_rows
|
def total_rows
|
||||||
|
execute['total_rows']
|
||||||
end
|
end
|
||||||
|
|
||||||
def rows
|
def rows
|
||||||
|
@rows ||= execute['rows'].map{|v| ViewRow.new(v, model)}
|
||||||
|
end
|
||||||
|
|
||||||
|
def keys
|
||||||
|
execute['rows'].map{|r| r.key}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
|
@ -79,6 +79,9 @@ module CouchRest
|
||||||
# are combined in an incorrect fashion.
|
# are combined in an incorrect fashion.
|
||||||
#
|
#
|
||||||
|
|
||||||
|
def database(value)
|
||||||
|
update_query(:database => value)
|
||||||
|
end
|
||||||
|
|
||||||
# Find all entries in the index whose key matches the value provided.
|
# Find all entries in the index whose key matches the value provided.
|
||||||
#
|
#
|
||||||
|
@ -104,7 +107,7 @@ module CouchRest
|
||||||
# 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+ call
|
||||||
# or a string.
|
# or a string.
|
||||||
def startkey_doc(value)
|
def startkey_doc(value)
|
||||||
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.
|
||||||
|
@ -119,7 +122,7 @@ module CouchRest
|
||||||
# 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+ call
|
||||||
# or a string.
|
# 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
|
||||||
|
|
||||||
|
|
||||||
|
@ -179,13 +182,88 @@ module CouchRest
|
||||||
update_query(:include_docs => true)
|
update_query(:include_docs => true)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
||||||
def execute(&block)
|
def execute(&block)
|
||||||
|
self.result ||= model.view(name, query, &block)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Class Methods
|
||||||
|
class << self
|
||||||
|
|
||||||
|
# Simplified view creation. A new view will be added to the
|
||||||
|
# provided model's design document using the name and options.
|
||||||
|
#
|
||||||
|
# If the view name starts with "by_" and +:by+ is not provided in
|
||||||
|
# the options, the new view's map method will be interpretted and
|
||||||
|
# generated automatically. For example:
|
||||||
|
#
|
||||||
|
# View.create(Meeting, "by_date_and_name")
|
||||||
|
#
|
||||||
|
# Will create a view that searches by the date and name properties.
|
||||||
|
# Explicity setting the attributes to use is possible using the
|
||||||
|
# +:by+ option. For example:
|
||||||
|
#
|
||||||
|
# View.create(Meeting, "by_date_and_name", :by => [:date, :firstname, :lastname])
|
||||||
|
#
|
||||||
|
# The view name is the same, but three keys would be used in the
|
||||||
|
# subsecuent index.
|
||||||
|
#
|
||||||
|
def create(model, name, opts = {})
|
||||||
|
views = model.design_doc['views'] ||= {}
|
||||||
|
|
||||||
|
unless opts[:map]
|
||||||
|
if opts[:by].nil? && name =~ /^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] ||= []
|
||||||
|
opts[:guards].push "(doc['#{model.model_type_key}'] == '#{model.to_s}')"
|
||||||
|
|
||||||
|
keys = opts[:by].map{|o| "doc['#{o}']"}
|
||||||
|
emit = keys.length == 1 ? keys.first : "[#{keys.join(', ')}]"
|
||||||
|
opts[:map] =
|
||||||
|
"function(doc) {" +
|
||||||
|
" if (#{opts[:guards].join(' && ')}) {" +
|
||||||
|
" emit(#{emit}, null);" +
|
||||||
|
" }" +
|
||||||
|
"}"
|
||||||
|
end
|
||||||
|
|
||||||
|
views[name.to_s] = {
|
||||||
|
:map => opts[:map],
|
||||||
|
:reduce => opts[:reduce] || false,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
# A special wrapper class that provides easy access to the key
|
||||||
|
# fields in a result row.
|
||||||
|
class ViewRow < Hash
|
||||||
|
attr_accessor :model
|
||||||
|
def initialize(hash, model)
|
||||||
|
self.model = model
|
||||||
|
super(hash)
|
||||||
|
end
|
||||||
|
def id
|
||||||
|
["id"]
|
||||||
|
end
|
||||||
|
def key
|
||||||
|
["key"]
|
||||||
|
end
|
||||||
|
def value
|
||||||
|
['value']
|
||||||
|
end
|
||||||
|
# Send a request for the linked document either using the "id" field's
|
||||||
|
# value, or the ["value"]["_id"] used for linked documents.
|
||||||
|
def doc
|
||||||
|
doc_id = value['_id'] || self.id
|
||||||
|
model.get(doc_id)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
|
|
@ -40,6 +40,8 @@ require "couchrest/model/class_proxy"
|
||||||
require "couchrest/model/collection"
|
require "couchrest/model/collection"
|
||||||
require "couchrest/model/associations"
|
require "couchrest/model/associations"
|
||||||
require "couchrest/model/configuration"
|
require "couchrest/model/configuration"
|
||||||
|
require "couchrest/model/design"
|
||||||
|
require "couchrest/model/design/view"
|
||||||
|
|
||||||
# Monkey patches applied to couchrest
|
# Monkey patches applied to couchrest
|
||||||
require "couchrest/model/support/couchrest"
|
require "couchrest/model/support/couchrest"
|
||||||
|
|
|
@ -1,9 +1,116 @@
|
||||||
require File.expand_path("../../spec_helper", __FILE__)
|
require File.expand_path("../../../spec_helper", __FILE__)
|
||||||
|
|
||||||
|
class DesignViewModel < CouchRest::Model::Base
|
||||||
|
property :name
|
||||||
|
|
||||||
|
design do
|
||||||
|
view :by_name
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe "Design View" do
|
describe "Design View" do
|
||||||
|
|
||||||
|
before :each do
|
||||||
|
@klass = CouchRest::Model::Design::View
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".new" do
|
||||||
|
|
||||||
|
describe "with invalid parent model" do
|
||||||
|
it "should burn" do
|
||||||
|
lambda { @klass.new(String) }.should raise_exception
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
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 ".create" do
|
||||||
|
|
||||||
|
before :all do
|
||||||
|
@design_doc = {}
|
||||||
|
DesignViewModel.stub!(:design_doc).and_return(@design_doc)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should add a basic view" do
|
||||||
|
@klass.create(DesignViewModel, 'test_view')
|
||||||
|
@design_doc['test_view'].should_not be_nil
|
||||||
|
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
|
||||||
|
@objs = [
|
||||||
|
{:name => "Sam"},
|
||||||
|
{:name => "Lorena"},
|
||||||
|
{:name => "Peter"},
|
||||||
|
{:name => "Judith"},
|
||||||
|
{:name => "Vilma"}
|
||||||
|
].map{|h| DesignViewModel.create(h)}
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "just documents" do
|
||||||
|
|
||||||
|
it "should return all" do
|
||||||
|
DesignViewModel.by_name.all.last.name.should eql("Vilma")
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
78
spec/couchrest/design_spec.rb
Normal file
78
spec/couchrest/design_spec.rb
Normal file
|
@ -0,0 +1,78 @@
|
||||||
|
require File.expand_path("../../spec_helper", __FILE__)
|
||||||
|
|
||||||
|
class DesignModel < CouchRest::Model::Base
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "Design" do
|
||||||
|
|
||||||
|
it "should accessable from model" do
|
||||||
|
DesignModel.respond_to?(:design).should be_true
|
||||||
|
end
|
||||||
|
|
||||||
|
describe ".design" do
|
||||||
|
|
||||||
|
it "should instantiate a new DesignMapper" do
|
||||||
|
CouchRest::Model::Design::DesignMapper.should_receive(:new).and_return(DesignModel)
|
||||||
|
DesignModel.design() { }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should instantiate a new DesignMapper with model" do
|
||||||
|
CouchRest::Model::Design::DesignMapper.should_receive(:new).with(DesignModel).and_return(DesignModel)
|
||||||
|
DesignModel.design() { }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should allow methods to be called in mapper" do
|
||||||
|
model = mock('Foo')
|
||||||
|
model.should_receive(:foo)
|
||||||
|
CouchRest::Model::Design::DesignMapper.stub!(:new).and_return(model)
|
||||||
|
DesignModel.design { foo }
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should request a design refresh" do
|
||||||
|
DesignModel.should_receive(:req_design_doc_refresh)
|
||||||
|
DesignModel.design() { }
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "DesignMapper" do
|
||||||
|
|
||||||
|
before :all do
|
||||||
|
@klass = CouchRest::Model::Design::DesignMapper
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should initialize and set model" do
|
||||||
|
object = @klass.new(DesignModel)
|
||||||
|
object.send(:model).should eql(DesignModel)
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "#view" do
|
||||||
|
|
||||||
|
before :each do
|
||||||
|
@object = @klass.new(DesignModel)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should call create method on view" do
|
||||||
|
CouchRest::Model::Design::View.should_receive(:create).with(DesignModel, 'test', {})
|
||||||
|
@object.view('test')
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should create a method on parent model" do
|
||||||
|
CouchRest::Model::Design::View.stub!(:create)
|
||||||
|
@object.view('test_view')
|
||||||
|
DesignModel.should respond_to(:test_view)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should create a method that returns view instance" do
|
||||||
|
CouchRest::Model::Design::View.stub!(:create)
|
||||||
|
@object.view('test_view')
|
||||||
|
CouchRest::Model::Design::View.should_receive(:new).with(DesignModel, {}, 'test_view').and_return(nil)
|
||||||
|
DesignModel.test_view
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
end
|
Loading…
Reference in a new issue