diff --git a/lib/couchrest/model/base.rb b/lib/couchrest/model/base.rb index b52ad59..f555c9f 100644 --- a/lib/couchrest/model/base.rb +++ b/lib/couchrest/model/base.rb @@ -16,6 +16,7 @@ module CouchRest include CouchRest::Model::PropertyProtection include CouchRest::Model::Associations include CouchRest::Model::Validations + include CouchRest::Model::Design def self.subclasses @subclasses ||= [] diff --git a/lib/couchrest/model/design.rb b/lib/couchrest/model/design.rb index 1bec004..9249d31 100644 --- a/lib/couchrest/model/design.rb +++ b/lib/couchrest/model/design.rb @@ -22,18 +22,32 @@ module CouchRest module ClassMethods def design(*args, &block) - + mapper = DesignMapper.new(self) + mapper.instance_eval(&block) + req_design_doc_refresh 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 diff --git a/lib/couchrest/model/design/view.rb b/lib/couchrest/model/design/view.rb index d1efe69..5196703 100644 --- a/lib/couchrest/model/design/view.rb +++ b/lib/couchrest/model/design/view.rb @@ -2,8 +2,6 @@ module CouchRest module Model module Design - # - # ### NOTE Work in progress! Not yet used! ### # # A proxy class that allows view queries to be created using # 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 # a normal relational database are not possible. At least not yet! # - # - # 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. 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? - @name = name - @database = parent.database - @query = { :reduce => false } - elsif parent.is_a? View - @database = parent.database - @query = parent.query.dup + self.model = parent + self.name = name + # Default options: + self.query = { :reduce => false } + elsif parent.is_a?(self.class) + self.model = 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) + query.update(new_query) super end @@ -44,31 +42,33 @@ module CouchRest # Inmediatly send a request to the database for all documents provided by the query. # def all(&block) - args = include_docs.query - + include_docs.execute(&block) end # Inmediatly send a request for the first result of the dataset. This will override # any limit set in the view previously. - def first(&block) - args = limit(1).include_docs.query - + def first + limit(1).include_docs.execute.first end def info - + end def offset - + execute['offset'] end def total_rows - + execute['total_rows'] end def rows + @rows ||= execute['rows'].map{|v| ViewRow.new(v, model)} + end + def keys + execute['rows'].map{|r| r.key} end @@ -78,7 +78,10 @@ module CouchRest # modified appropriatly. Errors will be raised if the methods # 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. # @@ -104,7 +107,7 @@ module CouchRest # The value may be provided as an object that responds to the +#id+ call # or a string. 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 # 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 # or a string. 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 @@ -179,13 +182,88 @@ module CouchRest update_query(:include_docs => true) end - 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 diff --git a/lib/couchrest_model.rb b/lib/couchrest_model.rb index 2e0070d..a0e3069 100644 --- a/lib/couchrest_model.rb +++ b/lib/couchrest_model.rb @@ -40,6 +40,8 @@ require "couchrest/model/class_proxy" require "couchrest/model/collection" require "couchrest/model/associations" require "couchrest/model/configuration" +require "couchrest/model/design" +require "couchrest/model/design/view" # Monkey patches applied to couchrest require "couchrest/model/support/couchrest" diff --git a/spec/couchrest/design/view_spec.rb b/spec/couchrest/design/view_spec.rb index f7df658..de8fb40 100644 --- a/spec/couchrest/design/view_spec.rb +++ b/spec/couchrest/design/view_spec.rb @@ -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 + 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 diff --git a/spec/couchrest/design_spec.rb b/spec/couchrest/design_spec.rb new file mode 100644 index 0000000..5543964 --- /dev/null +++ b/spec/couchrest/design_spec.rb @@ -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