From 9ee697c533d2d1c0093a8621313fcc4f19f8e575 Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Tue, 20 Jan 2015 18:49:06 +0000 Subject: [PATCH 01/21] [back_assignments] create fixture models and spec titles --- spec/fixtures/models/husband.rb | 7 +++++++ spec/fixtures/models/kid.rb | 7 +++++++ spec/fixtures/models/sale_entry.rb | 2 +- spec/fixtures/models/wife.rb | 7 +++++++ spec/unit/assocations_dual_spec.rb | 22 ++++++++++++++++++++++ spec/unit/assocations_spec.rb | 2 +- 6 files changed, 45 insertions(+), 2 deletions(-) create mode 100644 spec/fixtures/models/husband.rb create mode 100644 spec/fixtures/models/kid.rb create mode 100644 spec/fixtures/models/wife.rb create mode 100644 spec/unit/assocations_dual_spec.rb diff --git a/spec/fixtures/models/husband.rb b/spec/fixtures/models/husband.rb new file mode 100644 index 00000000..d5434fa4 --- /dev/null +++ b/spec/fixtures/models/husband.rb @@ -0,0 +1,7 @@ +class Husband < CouchRest::Model::Base + property :name, [String] + + belongs_to :wife + + collection_of :children, :class_name => 'Kid' +end diff --git a/spec/fixtures/models/kid.rb b/spec/fixtures/models/kid.rb new file mode 100644 index 00000000..bb81baff --- /dev/null +++ b/spec/fixtures/models/kid.rb @@ -0,0 +1,7 @@ +class Kid < CouchRest::Model::Base + property :name, [String] + + belongs_to :dad, :class_name => 'Husband' + belongs_to :mum, :class_name => 'Wife' + +end diff --git a/spec/fixtures/models/sale_entry.rb b/spec/fixtures/models/sale_entry.rb index 4c4b8e38..30741dbc 100644 --- a/spec/fixtures/models/sale_entry.rb +++ b/spec/fixtures/models/sale_entry.rb @@ -7,5 +7,5 @@ class SaleEntry < CouchRest::Model::Base design do view :by_description end - + end diff --git a/spec/fixtures/models/wife.rb b/spec/fixtures/models/wife.rb new file mode 100644 index 00000000..b24f5f8c --- /dev/null +++ b/spec/fixtures/models/wife.rb @@ -0,0 +1,7 @@ +class Wife < CouchRest::Model::Base + property :name, [String] + + belongs_to :husband + + collection_of :children, :class_name => 'Kid' +end diff --git a/spec/unit/assocations_dual_spec.rb b/spec/unit/assocations_dual_spec.rb new file mode 100644 index 00000000..798a1ffc --- /dev/null +++ b/spec/unit/assocations_dual_spec.rb @@ -0,0 +1,22 @@ +# encoding: utf-8 +require 'spec_helper' + +describe 'Associations' do + + describe 'of type belongs' do + context 'the other side also belongs_to' do + it 'should set the other side property too' do + end + end + + context 'the other side do not share association' do + it 'should set property without error' do + end + end + + context 'the other side associate as a collection' do + it 'should be part of the collection weh setting the property' do + end + end + end +end diff --git a/spec/unit/assocations_spec.rb b/spec/unit/assocations_spec.rb index 04e95039..2582bf3d 100644 --- a/spec/unit/assocations_spec.rb +++ b/spec/unit/assocations_spec.rb @@ -29,7 +29,7 @@ def SaleInvoice.merge_assoc_opts(*args) o = SaleInvoice.merge_assoc_opts(:cat) o[:proxy].should eql('self.company.cats') end - + end describe "of type belongs to" do From 9fdeae50923a0d8a7e0b3ef65d32df55de0cbcd7 Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Tue, 20 Jan 2015 18:59:10 +0000 Subject: [PATCH 02/21] [back_assignments] finish spec cases enumeration --- spec/unit/assocations_dual_spec.rb | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/spec/unit/assocations_dual_spec.rb b/spec/unit/assocations_dual_spec.rb index 798a1ffc..7d437d79 100644 --- a/spec/unit/assocations_dual_spec.rb +++ b/spec/unit/assocations_dual_spec.rb @@ -3,20 +3,31 @@ describe 'Associations' do - describe 'of type belongs' do - context 'the other side also belongs_to' do + describe 'of type belongs_to' do + context 'the other side also belongs_to (1-1)' do it 'should set the other side property too' do end end - context 'the other side do not share association' do + context 'the other side do not back associate (1-0)' do it 'should set property without error' do end end - context 'the other side associate as a collection' do + context 'the other side associate as a collection (1-n)' do it 'should be part of the collection weh setting the property' do end end end + + descibe 'of type collection_of' do + context 'the other side is a belongs_to (n-1)' do + it 'should populate the belongs_to property when added to the collection' + end + + context 'the other side do not back associate (n-0)' do + it 'should set property without error' do + end + end + end end From 8dc8f44dba27561ba2cd21a29681b1df3de9e508 Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Tue, 20 Jan 2015 21:38:04 +0000 Subject: [PATCH 03/21] [back_assignments] simplier name for fixture models --- spec/fixtures/models/husband.rb | 2 +- spec/fixtures/models/kid.rb | 2 +- spec/fixtures/models/wife.rb | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/fixtures/models/husband.rb b/spec/fixtures/models/husband.rb index d5434fa4..291e33bc 100644 --- a/spec/fixtures/models/husband.rb +++ b/spec/fixtures/models/husband.rb @@ -1,5 +1,5 @@ class Husband < CouchRest::Model::Base - property :name, [String] + property :name, String belongs_to :wife diff --git a/spec/fixtures/models/kid.rb b/spec/fixtures/models/kid.rb index bb81baff..bb125870 100644 --- a/spec/fixtures/models/kid.rb +++ b/spec/fixtures/models/kid.rb @@ -1,5 +1,5 @@ class Kid < CouchRest::Model::Base - property :name, [String] + property :name, String belongs_to :dad, :class_name => 'Husband' belongs_to :mum, :class_name => 'Wife' diff --git a/spec/fixtures/models/wife.rb b/spec/fixtures/models/wife.rb index b24f5f8c..8aa06308 100644 --- a/spec/fixtures/models/wife.rb +++ b/spec/fixtures/models/wife.rb @@ -1,5 +1,5 @@ class Wife < CouchRest::Model::Base - property :name, [String] + property :name, String belongs_to :husband From 0bdf3f12c76ffa062d49c084d00721731b00b8b7 Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Tue, 20 Jan 2015 21:38:29 +0000 Subject: [PATCH 04/21] [back_assignments] make examples 'not implemented yet' --- spec/unit/assocations_dual_spec.rb | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/spec/unit/assocations_dual_spec.rb b/spec/unit/assocations_dual_spec.rb index 7d437d79..cb686571 100644 --- a/spec/unit/assocations_dual_spec.rb +++ b/spec/unit/assocations_dual_spec.rb @@ -5,29 +5,26 @@ describe 'of type belongs_to' do context 'the other side also belongs_to (1-1)' do - it 'should set the other side property too' do - end + it 'should set the other side property too' end context 'the other side do not back associate (1-0)' do - it 'should set property without error' do - end + it 'should set property without error' + end context 'the other side associate as a collection (1-n)' do - it 'should be part of the collection weh setting the property' do - end + it 'should be part of the collection weh setting the property' end end - descibe 'of type collection_of' do + describe 'of type collection_of' do context 'the other side is a belongs_to (n-1)' do it 'should populate the belongs_to property when added to the collection' end context 'the other side do not back associate (n-0)' do - it 'should set property without error' do - end + it 'should set property without error' end end end From e48dd9f7df597f0deb1060add3e4eac135456b3a Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Tue, 20 Jan 2015 21:40:06 +0000 Subject: [PATCH 05/21] [back_assignments] partial try to remember associations --- lib/couchrest/model/associations.rb | 18 ++++++++++++++---- spec/unit/assocations_dual_spec.rb | 9 ++++++++- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/couchrest/model/associations.rb b/lib/couchrest/model/associations.rb index 6916069f..9b93cf9a 100644 --- a/lib/couchrest/model/associations.rb +++ b/lib/couchrest/model/associations.rb @@ -3,11 +3,13 @@ module Model module Associations # Basic support for relationships between CouchRest::Model::Base - + def self.included(base) base.extend(ClassMethods) end + Association = Struct.new(:type, :attribute, :options, :target) + module ClassMethods # Define an association that this object belongs to. @@ -15,14 +17,14 @@ module ClassMethods # An attribute will be created matching the name of the attribute # with '_id' on the end, or the foreign key (:foreign_key) provided. # - # Searching for the assocated object is performed using a string + # Searching for the assocated object is performed using a string # (:proxy) to be evaulated in the context of the owner. Typically # this will be set to the class name (:class_name), or determined # automatically if the owner belongs to a proxy object. # # If the association owner is proxied by another model, than an attempt will # be made to automatically determine the correct place to request - # the documents. Typically, this is a method with the pluralized name of the + # the documents. Typically, this is a method with the pluralized name of the # association inside owner's owner, or proxy. # # For example, imagine a company acts as a proxy for invoices and clients. @@ -32,7 +34,7 @@ module ClassMethods # # self.company.clients # - # If the name of the collection proxy is not the pluralized assocation name, + # If the name of the collection proxy is not the pluralized assocation name, # it can be set with the :proxy_name option. # def belongs_to(attrib, *options) @@ -40,6 +42,8 @@ def belongs_to(attrib, *options) property(opts[:foreign_key], String, opts) + associations.push(Association.new(:belongs_to, attrib, options, nil)) + create_association_property_setter(attrib, opts) create_belongs_to_getter(attrib, opts) create_belongs_to_setter(attrib, opts) @@ -91,6 +95,8 @@ def collection_of(attrib, *options) property(opts[:foreign_key], [String], opts) + associations.push(Association.new(:belongs_to, attrib, options, nil)) + create_association_property_setter(attrib, opts) create_collection_of_getter(attrib, opts) create_collection_of_setter(attrib, opts) @@ -99,6 +105,10 @@ def collection_of(attrib, *options) private + def associations + @_associations ||= [] + end + def merge_belongs_to_association_options(attrib, options = nil) opts = { :foreign_key => attrib.to_s.singularize + '_id', diff --git a/spec/unit/assocations_dual_spec.rb b/spec/unit/assocations_dual_spec.rb index cb686571..02a3b9fc 100644 --- a/spec/unit/assocations_dual_spec.rb +++ b/spec/unit/assocations_dual_spec.rb @@ -3,9 +3,16 @@ describe 'Associations' do + let(:father) { Husband.create(name: 'bob')} + let(:mummy) { Wife.create( name: 'claire')} + let(:kid) { Kid.create( name: 'Vladimir')} + describe 'of type belongs_to' do context 'the other side also belongs_to (1-1)' do - it 'should set the other side property too' + it 'should set the other side property too' do + father.wife = mummy + expect(mummy.husband).to eq(father) + end end context 'the other side do not back associate (1-0)' do From 6c8d3aead004b4f3e140ab66bfdc37aa54c2ec71 Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Tue, 20 Jan 2015 23:35:53 +0000 Subject: [PATCH 06/21] [back_assignments] belongs_to (1-1) back asignment works --- lib/couchrest/model/associations.rb | 14 ++++++++++---- spec/unit/assocations_dual_spec.rb | 2 +- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/couchrest/model/associations.rb b/lib/couchrest/model/associations.rb index 9b93cf9a..2676aa6b 100644 --- a/lib/couchrest/model/associations.rb +++ b/lib/couchrest/model/associations.rb @@ -42,7 +42,7 @@ def belongs_to(attrib, *options) property(opts[:foreign_key], String, opts) - associations.push(Association.new(:belongs_to, attrib, options, nil)) + associations.push(Association.new(:belongs_to, attrib, opts, nil)) create_association_property_setter(attrib, opts) create_belongs_to_getter(attrib, opts) @@ -95,7 +95,7 @@ def collection_of(attrib, *options) property(opts[:foreign_key], [String], opts) - associations.push(Association.new(:belongs_to, attrib, options, nil)) + associations.push(Association.new(:belongs_to, attrib, opts, nil)) create_association_property_setter(attrib, opts) create_collection_of_getter(attrib, opts) @@ -103,12 +103,12 @@ def collection_of(attrib, *options) end - private - def associations @_associations ||= [] end + private + def merge_belongs_to_association_options(attrib, options = nil) opts = { :foreign_key => attrib.to_s.singularize + '_id', @@ -157,6 +157,7 @@ def create_belongs_to_setter(attrib, options) class_eval <<-EOS, __FILE__, __LINE__ + 1 def #{attrib}=(value) self.#{options[:foreign_key]} = value.nil? ? nil : value.id + value.set_back_association(self, #{options}) @#{attrib} = value end EOS @@ -184,6 +185,11 @@ def #{attrib}=(value) end + def set_back_association(value, options) + assoc = self.class.associations.detect { |assoc| assoc[:options][:class_name] == value.class.name } + send("#{assoc[:options][:foreign_key]}=", (value.nil? ? nil : value.id)) + end + end # Special proxy for a collection of items so that adding and removing diff --git a/spec/unit/assocations_dual_spec.rb b/spec/unit/assocations_dual_spec.rb index 02a3b9fc..b3804367 100644 --- a/spec/unit/assocations_dual_spec.rb +++ b/spec/unit/assocations_dual_spec.rb @@ -11,7 +11,7 @@ context 'the other side also belongs_to (1-1)' do it 'should set the other side property too' do father.wife = mummy - expect(mummy.husband).to eq(father) + mummy.husband.should eql(father) end end From 1df987d13084172f2839148c890a90cc4160f263 Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Thu, 22 Jan 2015 21:37:51 +0000 Subject: [PATCH 07/21] [back_assignments] from belongs_to done --- lib/couchrest/model/associations.rb | 12 +++++++++--- spec/unit/assocations_dual_spec.rb | 11 +++++++++-- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/lib/couchrest/model/associations.rb b/lib/couchrest/model/associations.rb index 2676aa6b..4420e899 100644 --- a/lib/couchrest/model/associations.rb +++ b/lib/couchrest/model/associations.rb @@ -95,7 +95,7 @@ def collection_of(attrib, *options) property(opts[:foreign_key], [String], opts) - associations.push(Association.new(:belongs_to, attrib, opts, nil)) + associations.push(Association.new(:collection_of, attrib, opts, nil)) create_association_property_setter(attrib, opts) create_collection_of_getter(attrib, opts) @@ -186,8 +186,14 @@ def #{attrib}=(value) end def set_back_association(value, options) - assoc = self.class.associations.detect { |assoc| assoc[:options][:class_name] == value.class.name } - send("#{assoc[:options][:foreign_key]}=", (value.nil? ? nil : value.id)) + assoc = self.class.associations.detect { |ass| ass[:options][:class_name] == value.class.name } + return unless assoc + case assoc[:type] + when :belongs_to + send("#{assoc[:options][:foreign_key]}=", (value.nil? ? nil : value.id)) + when :collection_of + instance_eval("#{assoc[:options][:foreign_key]}.push('#{value.nil? ? nil : value.id}')") + end end end diff --git a/spec/unit/assocations_dual_spec.rb b/spec/unit/assocations_dual_spec.rb index b3804367..b0c621fb 100644 --- a/spec/unit/assocations_dual_spec.rb +++ b/spec/unit/assocations_dual_spec.rb @@ -16,12 +16,19 @@ end context 'the other side do not back associate (1-0)' do - it 'should set property without error' + let(:invoice) { SaleInvoice.create(:price => 2000) } + let(:client) { Client.create(:name => "Sam Lown") } + it 'should set property without error' do + lambda { invoice.client = client }.should_not raise_error + end end context 'the other side associate as a collection (1-n)' do - it 'should be part of the collection weh setting the property' + it 'should be part of the collection when setting the property' do + kid.dad = father + father.children.should include(kid) + end end end From 83ab20843e3b9075723d00295cb44f0597dee82e Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Sat, 24 Jan 2015 00:14:27 +0000 Subject: [PATCH 08/21] [back_assignments] - unset belongs_to when setting origin to nil - set/unset collection_of --- lib/couchrest/model/associations.rb | 21 +++++-- spec/unit/assocations_dual_spec.rb | 95 +++++++++++++++++++++++++++-- 2 files changed, 108 insertions(+), 8 deletions(-) diff --git a/lib/couchrest/model/associations.rb b/lib/couchrest/model/associations.rb index 4420e899..c59ef0b0 100644 --- a/lib/couchrest/model/associations.rb +++ b/lib/couchrest/model/associations.rb @@ -156,8 +156,13 @@ def #{attrib} def create_belongs_to_setter(attrib, options) class_eval <<-EOS, __FILE__, __LINE__ + 1 def #{attrib}=(value) - self.#{options[:foreign_key]} = value.nil? ? nil : value.id - value.set_back_association(self, #{options}) + old_binding = @#{attrib} + self.#{options[:foreign_key]} = value.nil? ? nil : value.id + unless value.nil? + value.set_back_association(self, self.class.name) + else + old_binding.set_back_association(nil, self.class.name) + end @#{attrib} = value end EOS @@ -185,8 +190,8 @@ def #{attrib}=(value) end - def set_back_association(value, options) - assoc = self.class.associations.detect { |ass| ass[:options][:class_name] == value.class.name } + def set_back_association(value, class_name) + assoc = self.class.associations.detect { |ass| ass[:options][:class_name] == class_name } return unless assoc case assoc[:type] when :belongs_to @@ -215,34 +220,42 @@ def initialize(array, property, parent) def << obj check_obj(obj) casted_by[casted_by_property.to_s] << obj.id + obj.set_back_association(casted_by, casted_by.class.name) super(obj) end def push(obj) check_obj(obj) casted_by[casted_by_property.to_s].push obj.id + obj.set_back_association(casted_by, casted_by.class.name) super(obj) end def unshift(obj) check_obj(obj) casted_by[casted_by_property.to_s].unshift obj.id + obj.set_back_association(casted_by, casted_by.class.name) super(obj) end def []= index, obj check_obj(obj) casted_by[casted_by_property.to_s][index] = obj.id + obj.set_back_association(casted_by, casted_by.class.name) super(index, obj) end def pop + obj = casted_by.send(casted_by_property.options[:proxy_name]).last casted_by[casted_by_property.to_s].pop + obj.set_back_association(nil, casted_by.class.name) super end def shift + obj = casted_by.send(casted_by_property.options[:proxy_name]).first casted_by[casted_by_property.to_s].shift + obj.set_back_association(nil, casted_by.class.name) super end diff --git a/spec/unit/assocations_dual_spec.rb b/spec/unit/assocations_dual_spec.rb index b0c621fb..ba2ac095 100644 --- a/spec/unit/assocations_dual_spec.rb +++ b/spec/unit/assocations_dual_spec.rb @@ -33,12 +33,99 @@ end describe 'of type collection_of' do - context 'the other side is a belongs_to (n-1)' do - it 'should populate the belongs_to property when added to the collection' + context 'with the other side is a belongs_to (n-1).' do + context 'Adding to the collection using <<' do + it 'should populate the belongs_to property' do + father.children << kid + kid.dad.should eq(father) + end + end + + context 'Adding to the collection using push' do + it 'should populate the belongs_to property' do + father.children.push kid + kid.dad.should eq(father) + end + end + + context 'Adding to the collection using unshift' do + it 'should populate the belongs_to property' do + father.children.unshift kid + kid.dad.should eq(father) + end + end + + context 'Adding to the collection using [n]=' do + it 'should populate the belongs_to property' do + father.children[3]= kid + kid.dad.should eq(father) + end + end + + context 'removing from the collection using pop' do + it 'should set nil the belongs_to property' do + father.children.push kid + father.children.pop + kid.dad.should be_nil + end + end + + context 'removing from the collection using shift' do + it 'should set nil the belongs_to property' do + father.children.push kid + father.children.shift + kid.dad.should be_nil + end + end + end - context 'the other side do not back associate (n-0)' do - it 'should set property without error' + context 'with the other side do not back associate (n-0)' do + let(:invoice) { SaleInvoice.create(:price => 2000) } + let(:entry) { SaleEntry.create(:description => 'test line 1', :price => 500) } + + context 'Adding to the collection using <<' do + it 'should set property without error' do + lambda { invoice.entries << entry}.should_not raise_error + end + end + + context 'Adding to the collection using push' do + it 'should set property without error' do + lambda { invoice.entries.push entry}.should_not raise_error + end + + context 'Adding to the collection using unshift' do + it 'should set property without error' do + lambda { invoice.entries.unshift entry}.should_not raise_error + end + end + + context 'Adding to the collection using []=' do + it 'should set property without error' do + lambda { invoice.entries[3]= entry}.should_not raise_error + end + end + + context 'removing from the collection using pop' do + it 'should set nil the belongs_to property' do + invoice.entries.push entry + lambda { invoice.entries.pop }.should_not raise_error + end + end + + context 'removing from the collection using shift' do + it 'should set nil the belongs_to property' do + invoice.entries.push entry + lambda { invoice.entries.shift }.should_not raise_error + end + end + + end end end + + describe 'when object is saved' do + it 'should also save other side' + end end From 8f0635598f7e1f5ccca7ad4b2bdb8920585f8eb2 Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Sun, 25 Jan 2015 22:54:39 +0000 Subject: [PATCH 09/21] [back_assignments] remove belongs_to other side when setting 1-1 assoc to nil --- spec/unit/assocations_dual_spec.rb | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/spec/unit/assocations_dual_spec.rb b/spec/unit/assocations_dual_spec.rb index ba2ac095..9e46d589 100644 --- a/spec/unit/assocations_dual_spec.rb +++ b/spec/unit/assocations_dual_spec.rb @@ -8,14 +8,20 @@ let(:kid) { Kid.create( name: 'Vladimir')} describe 'of type belongs_to' do - context 'the other side also belongs_to (1-1)' do + context 'with the other side also belongs_to (1-1)' do it 'should set the other side property too' do father.wife = mummy mummy.husband.should eql(father) end + + it 'should remove the other side if value is nil' do + father.wife = mummy + father.wife = nil + mummy.husband.should be_nil + end end - context 'the other side do not back associate (1-0)' do + context 'with the other side do not back associate (1-0)' do let(:invoice) { SaleInvoice.create(:price => 2000) } let(:client) { Client.create(:name => "Sam Lown") } it 'should set property without error' do @@ -24,7 +30,7 @@ end - context 'the other side associate as a collection (1-n)' do + context 'with the other side associate as a collection (1-n)' do it 'should be part of the collection when setting the property' do kid.dad = father father.children.should include(kid) From 98f7fedb910369acf2a35bc8a7765d2e84dc132b Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Sun, 25 Jan 2015 22:55:26 +0000 Subject: [PATCH 10/21] [back_assignments] put callbacks before association when including in model.base.rb to be able to define a after_save for dirty associations. --- lib/couchrest/model/base.rb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/couchrest/model/base.rb b/lib/couchrest/model/base.rb index d6acfd2c..ae0a7387 100644 --- a/lib/couchrest/model/base.rb +++ b/lib/couchrest/model/base.rb @@ -13,13 +13,13 @@ class Base < CouchRest::Document include ExtendedAttachments include Proxyable include PropertyProtection - include Associations include Validations include Callbacks + include Associations include Designs include CastedBy include Dirty - + def self.subclasses @subclasses ||= [] @@ -68,13 +68,13 @@ def initialize(attributes = {}, options = {}) alias :new_record? :new? alias :new_document? :new? - # Compare this model with another by confirming to see + # Compare this model with another by confirming to see # if the IDs and their databases match! # - # Camparison of the database is required in case the + # Camparison of the database is required in case the # model has been proxied or loaded elsewhere. # - # A Basic CouchRest document will only ever compare using + # A Basic CouchRest document will only ever compare using # a Hash comparison on the attributes. def == other return false unless other.is_a?(Base) From 4cd377d899418d08d5e4afc5eee68b9db64f3c8a Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Sun, 25 Jan 2015 23:40:46 +0000 Subject: [PATCH 11/21] [back_assignments] propagate save for 1-1 associations --- lib/couchrest/model/associations.rb | 27 ++++++++++++++++++++++----- spec/unit/assocations_dual_spec.rb | 12 +++++++++--- 2 files changed, 31 insertions(+), 8 deletions(-) diff --git a/lib/couchrest/model/associations.rb b/lib/couchrest/model/associations.rb index c59ef0b0..b177739a 100644 --- a/lib/couchrest/model/associations.rb +++ b/lib/couchrest/model/associations.rb @@ -1,11 +1,12 @@ module CouchRest module Model module Associations + extend ActiveSupport::Concern # Basic support for relationships between CouchRest::Model::Base - def self.included(base) - base.extend(ClassMethods) + included do + after_save :save_dirty_association if respond_to?(:after_save) end Association = Struct.new(:type, :attribute, :options, :target) @@ -156,13 +157,15 @@ def #{attrib} def create_belongs_to_setter(attrib, options) class_eval <<-EOS, __FILE__, __LINE__ + 1 def #{attrib}=(value) - old_binding = @#{attrib} + binding = @#{attrib} self.#{options[:foreign_key]} = value.nil? ? nil : value.id unless value.nil? - value.set_back_association(self, self.class.name) + binding = value + binding.set_back_association(self, self.class.name) else - old_binding.set_back_association(nil, self.class.name) + binding.set_back_association(nil, self.class.name) end + register_dirty_association(binding) @#{attrib} = value end EOS @@ -201,6 +204,20 @@ def set_back_association(value, class_name) end end + def dirty_associations + @_dirty_associations ||= [] + end + + def register_dirty_association(obj) + dirty_associations << obj unless @_dirty_associations.include?(obj) + end + + def save_dirty_association + dirty_associations.each do |obj| + obj.save + end + end + end # Special proxy for a collection of items so that adding and removing diff --git a/spec/unit/assocations_dual_spec.rb b/spec/unit/assocations_dual_spec.rb index 9e46d589..932c1f12 100644 --- a/spec/unit/assocations_dual_spec.rb +++ b/spec/unit/assocations_dual_spec.rb @@ -36,6 +36,15 @@ father.children.should include(kid) end end + + describe 'when object is saved' do + it 'should also save other side' do + father.wife = mummy + mummy.should_receive(:save) + father.save + end + end + end describe 'of type collection_of' do @@ -131,7 +140,4 @@ end end - describe 'when object is saved' do - it 'should also save other side' - end end From 330c1b87951d25f6813cdf5b9747830ea71093ad Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Mon, 26 Jan 2015 23:13:32 +0000 Subject: [PATCH 12/21] [back_assignments] - do not call save more than once - propagate save for collection_of associations --- lib/couchrest/model/associations.rb | 9 ++- spec/unit/assocations_dual_spec.rb | 92 ++++++++++++++++++++++------- 2 files changed, 80 insertions(+), 21 deletions(-) diff --git a/lib/couchrest/model/associations.rb b/lib/couchrest/model/associations.rb index b177739a..ef433513 100644 --- a/lib/couchrest/model/associations.rb +++ b/lib/couchrest/model/associations.rb @@ -213,7 +213,8 @@ def register_dirty_association(obj) end def save_dirty_association - dirty_associations.each do |obj| + while !dirty_associations.empty? do + obj = dirty_associations.pop obj.save end end @@ -238,6 +239,7 @@ def << obj check_obj(obj) casted_by[casted_by_property.to_s] << obj.id obj.set_back_association(casted_by, casted_by.class.name) + casted_by.register_dirty_association(obj) super(obj) end @@ -245,6 +247,7 @@ def push(obj) check_obj(obj) casted_by[casted_by_property.to_s].push obj.id obj.set_back_association(casted_by, casted_by.class.name) + casted_by.register_dirty_association(obj) super(obj) end @@ -252,6 +255,7 @@ def unshift(obj) check_obj(obj) casted_by[casted_by_property.to_s].unshift obj.id obj.set_back_association(casted_by, casted_by.class.name) + casted_by.register_dirty_association(obj) super(obj) end @@ -259,6 +263,7 @@ def []= index, obj check_obj(obj) casted_by[casted_by_property.to_s][index] = obj.id obj.set_back_association(casted_by, casted_by.class.name) + casted_by.register_dirty_association(obj) super(index, obj) end @@ -266,6 +271,7 @@ def pop obj = casted_by.send(casted_by_property.options[:proxy_name]).last casted_by[casted_by_property.to_s].pop obj.set_back_association(nil, casted_by.class.name) + casted_by.register_dirty_association(obj) super end @@ -273,6 +279,7 @@ def shift obj = casted_by.send(casted_by_property.options[:proxy_name]).first casted_by[casted_by_property.to_s].shift obj.set_back_association(nil, casted_by.class.name) + casted_by.register_dirty_association(obj) super end diff --git a/spec/unit/assocations_dual_spec.rb b/spec/unit/assocations_dual_spec.rb index 932c1f12..cae97339 100644 --- a/spec/unit/assocations_dual_spec.rb +++ b/spec/unit/assocations_dual_spec.rb @@ -43,8 +43,14 @@ mummy.should_receive(:save) father.save end - end - + it 'should not call save twice in a row' do + father.wife = mummy + mummy.should_receive(:save).exactly(1) + father.save + father.name = 'rogers' + father.save + end + end end describe 'of type collection_of' do @@ -54,6 +60,13 @@ father.children << kid kid.dad.should eq(father) end + describe 'when object is saved' do + it 'should also save other side' do + father.children << kid + kid.should_receive(:save) + father.save + end + end end context 'Adding to the collection using push' do @@ -61,6 +74,13 @@ father.children.push kid kid.dad.should eq(father) end + describe 'when object is saved' do + it 'should also save other side' do + father.children.push kid + kid.should_receive(:save) + father.save + end + end end context 'Adding to the collection using unshift' do @@ -68,6 +88,13 @@ father.children.unshift kid kid.dad.should eq(father) end + describe 'when object is saved' do + it 'should also save other side' do + father.children.unshift kid + kid.should_receive(:save) + father.save + end + end end context 'Adding to the collection using [n]=' do @@ -75,6 +102,13 @@ father.children[3]= kid kid.dad.should eq(father) end + describe 'when object is saved' do + it 'should also save other side' do + father.children[4] = kid + kid.should_receive(:save) + father.save + end + end end context 'removing from the collection using pop' do @@ -83,6 +117,15 @@ father.children.pop kid.dad.should be_nil end + describe 'when object is saved' do + it 'should also save other side' do + father.children.push kid + father.save + father.children.pop + kid.should_receive(:save) + father.save + end + end end context 'removing from the collection using shift' do @@ -91,6 +134,15 @@ father.children.shift kid.dad.should be_nil end + describe 'when object is saved' do + it 'should also save other side' do + father.children.push kid + father.save + father.children.shift + kid.should_receive(:save) + father.save + end + end end end @@ -110,31 +162,31 @@ lambda { invoice.entries.push entry}.should_not raise_error end - context 'Adding to the collection using unshift' do - it 'should set property without error' do - lambda { invoice.entries.unshift entry}.should_not raise_error + context 'Adding to the collection using unshift' do + it 'should set property without error' do + lambda { invoice.entries.unshift entry}.should_not raise_error + end end - end - context 'Adding to the collection using []=' do - it 'should set property without error' do - lambda { invoice.entries[3]= entry}.should_not raise_error + context 'Adding to the collection using []=' do + it 'should set property without error' do + lambda { invoice.entries[3]= entry}.should_not raise_error + end end - end - context 'removing from the collection using pop' do - it 'should set nil the belongs_to property' do - invoice.entries.push entry - lambda { invoice.entries.pop }.should_not raise_error + context 'removing from the collection using pop' do + it 'should set nil the belongs_to property' do + invoice.entries.push entry + lambda { invoice.entries.pop }.should_not raise_error + end end - end - context 'removing from the collection using shift' do - it 'should set nil the belongs_to property' do - invoice.entries.push entry - lambda { invoice.entries.shift }.should_not raise_error + context 'removing from the collection using shift' do + it 'should set nil the belongs_to property' do + invoice.entries.push entry + lambda { invoice.entries.shift }.should_not raise_error + end end - end end end From b97403b8ead703fc06c6c1e2ec20d72110ede0ae Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Wed, 11 Mar 2015 23:07:29 +0000 Subject: [PATCH 13/21] add option to support specifying reverse association field --- lib/couchrest/model/associations.rb | 33 +++++++++++++++++++---------- spec/fixtures/models/husband.rb | 7 ------ spec/fixtures/models/kid.rb | 4 ++-- spec/fixtures/models/parent.rb | 10 +++++++++ spec/fixtures/models/super_power.rb | 5 +++++ spec/fixtures/models/wife.rb | 7 ------ spec/unit/assocations_dual_spec.rb | 30 ++++++++++++++++++-------- 7 files changed, 60 insertions(+), 36 deletions(-) delete mode 100644 spec/fixtures/models/husband.rb create mode 100644 spec/fixtures/models/parent.rb create mode 100644 spec/fixtures/models/super_power.rb delete mode 100644 spec/fixtures/models/wife.rb diff --git a/lib/couchrest/model/associations.rb b/lib/couchrest/model/associations.rb index ef433513..88227810 100644 --- a/lib/couchrest/model/associations.rb +++ b/lib/couchrest/model/associations.rb @@ -111,9 +111,11 @@ def associations private def merge_belongs_to_association_options(attrib, options = nil) + class_name = options.delete(:class_name) if options.is_a?(Hash) + class_name ||= attrib opts = { :foreign_key => attrib.to_s.singularize + '_id', - :class_name => attrib.to_s.singularize.camelcase, + :class_name => class_name.to_s.singularize.camelcase, :proxy_name => attrib.to_s.pluralize, :allow_blank => false } @@ -161,9 +163,9 @@ def #{attrib}=(value) self.#{options[:foreign_key]} = value.nil? ? nil : value.id unless value.nil? binding = value - binding.set_back_association(self, self.class.name) + binding.set_back_association(self, self.class.name, '#{options[:reverse_association]}') else - binding.set_back_association(nil, self.class.name) + binding.set_back_association(nil, self.class.name, '#{options[:reverse_association]}') end register_dirty_association(binding) @#{attrib} = value @@ -193,14 +195,23 @@ def #{attrib}=(value) end - def set_back_association(value, class_name) - assoc = self.class.associations.detect { |ass| ass[:options][:class_name] == class_name } - return unless assoc - case assoc[:type] - when :belongs_to - send("#{assoc[:options][:foreign_key]}=", (value.nil? ? nil : value.id)) - when :collection_of - instance_eval("#{assoc[:options][:foreign_key]}.push('#{value.nil? ? nil : value.id}')") + def set_back_association(value, class_name, reverse_association = nil) + if reverse_association && !reverse_association.empty? + prop = self.class.properties.detect { |prop| prop.name =~ %r{#{reverse_association}_id} } + if prop.type.ancestors.include? Enumerable + instance_eval("#{prop.name}.push('#{value.nil? ? nil : value.id}')") + else + send("#{prop.name}=", (value.nil? ? nil : value.id)) + end + else + assoc = self.class.associations.detect { |ass| ass[:options][:class_name] == class_name } + return unless assoc + case assoc[:type] + when :belongs_to + send("#{assoc[:options][:foreign_key]}=", (value.nil? ? nil : value.id)) + when :collection_of + instance_eval("#{assoc[:options][:foreign_key]}.push('#{value.nil? ? nil : value.id}')") + end end end diff --git a/spec/fixtures/models/husband.rb b/spec/fixtures/models/husband.rb deleted file mode 100644 index 291e33bc..00000000 --- a/spec/fixtures/models/husband.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Husband < CouchRest::Model::Base - property :name, String - - belongs_to :wife - - collection_of :children, :class_name => 'Kid' -end diff --git a/spec/fixtures/models/kid.rb b/spec/fixtures/models/kid.rb index bb125870..518ef2d5 100644 --- a/spec/fixtures/models/kid.rb +++ b/spec/fixtures/models/kid.rb @@ -1,7 +1,7 @@ class Kid < CouchRest::Model::Base property :name, String - belongs_to :dad, :class_name => 'Husband' - belongs_to :mum, :class_name => 'Wife' + belongs_to :dad, :class_name => 'Parent' + belongs_to :mum, :class_name => 'Parent' end diff --git a/spec/fixtures/models/parent.rb b/spec/fixtures/models/parent.rb new file mode 100644 index 00000000..7820c117 --- /dev/null +++ b/spec/fixtures/models/parent.rb @@ -0,0 +1,10 @@ +class Parent < CouchRest::Model::Base + property :name, String + + belongs_to :super_power + belongs_to :husband, :class_name => :parent, :reverse_association => :wife + belongs_to :wife, :class_name => :parent, :reverse_association => :husband + belongs_to :lives_with, :class_name => :parent, :reverse_association => :lives_with + + collection_of :children, :class_name => 'Kid' +end diff --git a/spec/fixtures/models/super_power.rb b/spec/fixtures/models/super_power.rb new file mode 100644 index 00000000..3dc58449 --- /dev/null +++ b/spec/fixtures/models/super_power.rb @@ -0,0 +1,5 @@ +class SuperPower < CouchRest::Model::Base + property :description, String + + belongs_to :parent +end diff --git a/spec/fixtures/models/wife.rb b/spec/fixtures/models/wife.rb deleted file mode 100644 index 8aa06308..00000000 --- a/spec/fixtures/models/wife.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Wife < CouchRest::Model::Base - property :name, String - - belongs_to :husband - - collection_of :children, :class_name => 'Kid' -end diff --git a/spec/unit/assocations_dual_spec.rb b/spec/unit/assocations_dual_spec.rb index cae97339..88c12750 100644 --- a/spec/unit/assocations_dual_spec.rb +++ b/spec/unit/assocations_dual_spec.rb @@ -3,21 +3,32 @@ describe 'Associations' do - let(:father) { Husband.create(name: 'bob')} - let(:mummy) { Wife.create( name: 'claire')} + let(:father) { Parent.create(name: 'Bob')} + let(:can_fly){ SuperPower.create(description: 'Can fly when there is no cloud')} + let(:mummy) { Parent.create( name: 'Claire')} let(:kid) { Kid.create( name: 'Vladimir')} describe 'of type belongs_to' do context 'with the other side also belongs_to (1-1)' do - it 'should set the other side property too' do - father.wife = mummy - mummy.husband.should eql(father) + context '[non ambiguous association]' do + it 'should set the other side property too' do + father.super_power = can_fly + can_fly.parent.should eql father + end end - it 'should remove the other side if value is nil' do - father.wife = mummy - father.wife = nil - mummy.husband.should be_nil + context '[ambiguous association]' do + it 'should set the other side property too' do + father.wife = mummy + mummy.husband.should eql(father) + end + end + + context '[cyclic association]' do + it 'should set the other side property too' do + father.lives_with = mummy + mummy.lives_with.should eql(father) + end end end @@ -25,6 +36,7 @@ let(:invoice) { SaleInvoice.create(:price => 2000) } let(:client) { Client.create(:name => "Sam Lown") } it 'should set property without error' do + invoice.client = client lambda { invoice.client = client }.should_not raise_error end From 1fc6cf70b38475e8a9aaa73a21c847b4ae8812ef Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Wed, 11 Mar 2015 23:07:29 +0000 Subject: [PATCH 14/21] [back_assignments] add option to support specifying reverse association field --- lib/couchrest/model/associations.rb | 33 +++++++++++++++++++---------- spec/fixtures/models/husband.rb | 7 ------ spec/fixtures/models/kid.rb | 4 ++-- spec/fixtures/models/parent.rb | 10 +++++++++ spec/fixtures/models/super_power.rb | 5 +++++ spec/fixtures/models/wife.rb | 7 ------ spec/unit/assocations_dual_spec.rb | 30 ++++++++++++++++++-------- 7 files changed, 60 insertions(+), 36 deletions(-) delete mode 100644 spec/fixtures/models/husband.rb create mode 100644 spec/fixtures/models/parent.rb create mode 100644 spec/fixtures/models/super_power.rb delete mode 100644 spec/fixtures/models/wife.rb diff --git a/lib/couchrest/model/associations.rb b/lib/couchrest/model/associations.rb index ef433513..88227810 100644 --- a/lib/couchrest/model/associations.rb +++ b/lib/couchrest/model/associations.rb @@ -111,9 +111,11 @@ def associations private def merge_belongs_to_association_options(attrib, options = nil) + class_name = options.delete(:class_name) if options.is_a?(Hash) + class_name ||= attrib opts = { :foreign_key => attrib.to_s.singularize + '_id', - :class_name => attrib.to_s.singularize.camelcase, + :class_name => class_name.to_s.singularize.camelcase, :proxy_name => attrib.to_s.pluralize, :allow_blank => false } @@ -161,9 +163,9 @@ def #{attrib}=(value) self.#{options[:foreign_key]} = value.nil? ? nil : value.id unless value.nil? binding = value - binding.set_back_association(self, self.class.name) + binding.set_back_association(self, self.class.name, '#{options[:reverse_association]}') else - binding.set_back_association(nil, self.class.name) + binding.set_back_association(nil, self.class.name, '#{options[:reverse_association]}') end register_dirty_association(binding) @#{attrib} = value @@ -193,14 +195,23 @@ def #{attrib}=(value) end - def set_back_association(value, class_name) - assoc = self.class.associations.detect { |ass| ass[:options][:class_name] == class_name } - return unless assoc - case assoc[:type] - when :belongs_to - send("#{assoc[:options][:foreign_key]}=", (value.nil? ? nil : value.id)) - when :collection_of - instance_eval("#{assoc[:options][:foreign_key]}.push('#{value.nil? ? nil : value.id}')") + def set_back_association(value, class_name, reverse_association = nil) + if reverse_association && !reverse_association.empty? + prop = self.class.properties.detect { |prop| prop.name =~ %r{#{reverse_association}_id} } + if prop.type.ancestors.include? Enumerable + instance_eval("#{prop.name}.push('#{value.nil? ? nil : value.id}')") + else + send("#{prop.name}=", (value.nil? ? nil : value.id)) + end + else + assoc = self.class.associations.detect { |ass| ass[:options][:class_name] == class_name } + return unless assoc + case assoc[:type] + when :belongs_to + send("#{assoc[:options][:foreign_key]}=", (value.nil? ? nil : value.id)) + when :collection_of + instance_eval("#{assoc[:options][:foreign_key]}.push('#{value.nil? ? nil : value.id}')") + end end end diff --git a/spec/fixtures/models/husband.rb b/spec/fixtures/models/husband.rb deleted file mode 100644 index 291e33bc..00000000 --- a/spec/fixtures/models/husband.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Husband < CouchRest::Model::Base - property :name, String - - belongs_to :wife - - collection_of :children, :class_name => 'Kid' -end diff --git a/spec/fixtures/models/kid.rb b/spec/fixtures/models/kid.rb index bb125870..518ef2d5 100644 --- a/spec/fixtures/models/kid.rb +++ b/spec/fixtures/models/kid.rb @@ -1,7 +1,7 @@ class Kid < CouchRest::Model::Base property :name, String - belongs_to :dad, :class_name => 'Husband' - belongs_to :mum, :class_name => 'Wife' + belongs_to :dad, :class_name => 'Parent' + belongs_to :mum, :class_name => 'Parent' end diff --git a/spec/fixtures/models/parent.rb b/spec/fixtures/models/parent.rb new file mode 100644 index 00000000..7820c117 --- /dev/null +++ b/spec/fixtures/models/parent.rb @@ -0,0 +1,10 @@ +class Parent < CouchRest::Model::Base + property :name, String + + belongs_to :super_power + belongs_to :husband, :class_name => :parent, :reverse_association => :wife + belongs_to :wife, :class_name => :parent, :reverse_association => :husband + belongs_to :lives_with, :class_name => :parent, :reverse_association => :lives_with + + collection_of :children, :class_name => 'Kid' +end diff --git a/spec/fixtures/models/super_power.rb b/spec/fixtures/models/super_power.rb new file mode 100644 index 00000000..3dc58449 --- /dev/null +++ b/spec/fixtures/models/super_power.rb @@ -0,0 +1,5 @@ +class SuperPower < CouchRest::Model::Base + property :description, String + + belongs_to :parent +end diff --git a/spec/fixtures/models/wife.rb b/spec/fixtures/models/wife.rb deleted file mode 100644 index 8aa06308..00000000 --- a/spec/fixtures/models/wife.rb +++ /dev/null @@ -1,7 +0,0 @@ -class Wife < CouchRest::Model::Base - property :name, String - - belongs_to :husband - - collection_of :children, :class_name => 'Kid' -end diff --git a/spec/unit/assocations_dual_spec.rb b/spec/unit/assocations_dual_spec.rb index cae97339..88c12750 100644 --- a/spec/unit/assocations_dual_spec.rb +++ b/spec/unit/assocations_dual_spec.rb @@ -3,21 +3,32 @@ describe 'Associations' do - let(:father) { Husband.create(name: 'bob')} - let(:mummy) { Wife.create( name: 'claire')} + let(:father) { Parent.create(name: 'Bob')} + let(:can_fly){ SuperPower.create(description: 'Can fly when there is no cloud')} + let(:mummy) { Parent.create( name: 'Claire')} let(:kid) { Kid.create( name: 'Vladimir')} describe 'of type belongs_to' do context 'with the other side also belongs_to (1-1)' do - it 'should set the other side property too' do - father.wife = mummy - mummy.husband.should eql(father) + context '[non ambiguous association]' do + it 'should set the other side property too' do + father.super_power = can_fly + can_fly.parent.should eql father + end end - it 'should remove the other side if value is nil' do - father.wife = mummy - father.wife = nil - mummy.husband.should be_nil + context '[ambiguous association]' do + it 'should set the other side property too' do + father.wife = mummy + mummy.husband.should eql(father) + end + end + + context '[cyclic association]' do + it 'should set the other side property too' do + father.lives_with = mummy + mummy.lives_with.should eql(father) + end end end @@ -25,6 +36,7 @@ let(:invoice) { SaleInvoice.create(:price => 2000) } let(:client) { Client.create(:name => "Sam Lown") } it 'should set property without error' do + invoice.client = client lambda { invoice.client = client }.should_not raise_error end From 9d54af6b2fb5ab171e6c6c7acd27c12e3bbceb12 Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Fri, 13 Mar 2015 20:34:02 +0000 Subject: [PATCH 15/21] [back_assignments] add reverse_association on collections --- lib/couchrest/model/associations.rb | 33 +++++++++++------------- spec/fixtures/models/parent.rb | 1 + spec/unit/assocations_dual_spec.rb | 39 +++++++++++++++++++++++++++-- 3 files changed, 53 insertions(+), 20 deletions(-) diff --git a/lib/couchrest/model/associations.rb b/lib/couchrest/model/associations.rb index 88227810..45efead4 100644 --- a/lib/couchrest/model/associations.rb +++ b/lib/couchrest/model/associations.rb @@ -247,41 +247,29 @@ def initialize(array, property, parent) end def << obj - check_obj(obj) - casted_by[casted_by_property.to_s] << obj.id - obj.set_back_association(casted_by, casted_by.class.name) - casted_by.register_dirty_association(obj) + add_to_collection_with(:<<, obj) super(obj) end def push(obj) - check_obj(obj) - casted_by[casted_by_property.to_s].push obj.id - obj.set_back_association(casted_by, casted_by.class.name) - casted_by.register_dirty_association(obj) + add_to_collection_with(:push, obj) super(obj) end def unshift(obj) - check_obj(obj) - casted_by[casted_by_property.to_s].unshift obj.id - obj.set_back_association(casted_by, casted_by.class.name) - casted_by.register_dirty_association(obj) + add_to_collection_with(:unshift, obj) super(obj) end def []= index, obj - check_obj(obj) - casted_by[casted_by_property.to_s][index] = obj.id - obj.set_back_association(casted_by, casted_by.class.name) - casted_by.register_dirty_association(obj) + add_to_collection_with(:[]=, obj, index) super(index, obj) end def pop obj = casted_by.send(casted_by_property.options[:proxy_name]).last casted_by[casted_by_property.to_s].pop - obj.set_back_association(nil, casted_by.class.name) + obj.set_back_association(nil, casted_by.class.name, casted_by_property.options[:reverse_association]) casted_by.register_dirty_association(obj) super end @@ -289,7 +277,7 @@ def pop def shift obj = casted_by.send(casted_by_property.options[:proxy_name]).first casted_by[casted_by_property.to_s].shift - obj.set_back_association(nil, casted_by.class.name) + obj.set_back_association(nil, casted_by.class.name, casted_by_property.options[:reverse_association]) casted_by.register_dirty_association(obj) super end @@ -300,6 +288,15 @@ def check_obj(obj) raise "Object cannot be added to #{casted_by.class.to_s}##{casted_by_property.to_s} collection unless saved" if obj.new? end + def add_to_collection_with(method, obj, index=nil) + check_obj(obj) + args = [ obj.id ] + args = args.insert(0, index) if index + casted_by[casted_by_property.to_s].send(method, *args) + obj.set_back_association(casted_by, casted_by.class.name, casted_by_property.options[:reverse_association]) + casted_by.register_dirty_association(obj) + end + # Override CastedArray instantiation_and_cast method for a simpler # version that will not try to cast the model. def instantiate_and_cast(obj, change = true) diff --git a/spec/fixtures/models/parent.rb b/spec/fixtures/models/parent.rb index 7820c117..ee1418f5 100644 --- a/spec/fixtures/models/parent.rb +++ b/spec/fixtures/models/parent.rb @@ -7,4 +7,5 @@ class Parent < CouchRest::Model::Base belongs_to :lives_with, :class_name => :parent, :reverse_association => :lives_with collection_of :children, :class_name => 'Kid' + collection_of :pets , :reverse_association => :owner end diff --git a/spec/unit/assocations_dual_spec.rb b/spec/unit/assocations_dual_spec.rb index 88c12750..89657f79 100644 --- a/spec/unit/assocations_dual_spec.rb +++ b/spec/unit/assocations_dual_spec.rb @@ -5,8 +5,9 @@ let(:father) { Parent.create(name: 'Bob')} let(:can_fly){ SuperPower.create(description: 'Can fly when there is no cloud')} - let(:mummy) { Parent.create( name: 'Claire')} - let(:kid) { Kid.create( name: 'Vladimir')} + let(:mummy) { Parent.create(name: 'Claire')} + let(:kid) { Kid.create( name: 'Vladimir')} + let(:dog) { Pet.create( name: 'Valdo' )} describe 'of type belongs_to' do context 'with the other side also belongs_to (1-1)' do @@ -72,6 +73,12 @@ father.children << kid kid.dad.should eq(father) end + context 'when reverse_association is specified' do + it 'should populate the belongs_to property' do + father.pets << dog + dog.owner.should eq(father) + end + end describe 'when object is saved' do it 'should also save other side' do father.children << kid @@ -86,6 +93,12 @@ father.children.push kid kid.dad.should eq(father) end + context 'when reverse_association is specified' do + it 'should populate the belongs_to property' do + father.pets.push dog + dog.owner.should eq(father) + end + end describe 'when object is saved' do it 'should also save other side' do father.children.push kid @@ -100,6 +113,10 @@ father.children.unshift kid kid.dad.should eq(father) end + it 'should populate the belongs_to property' do + father.pets.unshift dog + dog.owner.should eq(father) + end describe 'when object is saved' do it 'should also save other side' do father.children.unshift kid @@ -114,6 +131,10 @@ father.children[3]= kid kid.dad.should eq(father) end + it 'should populate the belongs_to property' do + father.pets[3] = dog + dog.owner.should eq(father) + end describe 'when object is saved' do it 'should also save other side' do father.children[4] = kid @@ -129,6 +150,13 @@ father.children.pop kid.dad.should be_nil end + context 'specifying reverse association' do + it 'should set nil the belongs_to property' do + father.pets.push dog + father.pets.pop + dog.owner.should be_nil + end + end describe 'when object is saved' do it 'should also save other side' do father.children.push kid @@ -146,6 +174,13 @@ father.children.shift kid.dad.should be_nil end + context 'specifying reverse association' do + it 'should set nil the belongs_to property' do + father.pets.push dog + father.pets.shift + dog.owner.should be_nil + end + end describe 'when object is saved' do it 'should also save other side' do father.children.push kid From a1d431cf88480b303c8e64439a0d6daf0dff1c20 Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Fri, 13 Mar 2015 22:46:02 +0000 Subject: [PATCH 16/21] [back_assignments] move CollectionOfProxy class in its own file --- lib/couchrest/model/associations.rb | 80 +----------------- .../model/associations/collection_of_proxy.rb | 81 +++++++++++++++++++ lib/couchrest_model.rb | 1 + spec/unit/assocations_spec.rb | 2 +- 4 files changed, 85 insertions(+), 79 deletions(-) create mode 100644 lib/couchrest/model/associations/collection_of_proxy.rb diff --git a/lib/couchrest/model/associations.rb b/lib/couchrest/model/associations.rb index 45efead4..02a7885c 100644 --- a/lib/couchrest/model/associations.rb +++ b/lib/couchrest/model/associations.rb @@ -180,7 +180,7 @@ def create_collection_of_getter(attrib, options) def #{attrib}(reload = false) return @#{attrib} unless @#{attrib}.nil? or reload ary = self.#{options[:foreign_key]}.collect{|i| #{options[:proxy]}.get(i)} - @#{attrib} = ::CouchRest::Model::CollectionOfProxy.new(ary, find_property('#{options[:foreign_key]}'), self) + @#{attrib} = ::CouchRest::Model::Associations::CollectionOfProxy.new(ary, find_property('#{options[:foreign_key]}'), self) end EOS end @@ -188,7 +188,7 @@ def #{attrib}(reload = false) def create_collection_of_setter(attrib, options) class_eval <<-EOS, __FILE__, __LINE__ + 1 def #{attrib}=(value) - @#{attrib} = ::CouchRest::Model::CollectionOfProxy.new(value, find_property('#{options[:foreign_key]}'), self) + @#{attrib} = ::CouchRest::Model::Associations::CollectionOfProxy.new(value, find_property('#{options[:foreign_key]}'), self) end EOS end @@ -232,82 +232,6 @@ def save_dirty_association end - # Special proxy for a collection of items so that adding and removing - # to the list automatically updates the associated property. - class CollectionOfProxy < CastedArray - - def initialize(array, property, parent) - (array ||= []).compact! - super(array, property, parent) - casted_by[casted_by_property.to_s] = [] # replace the original array! - array.compact.each do |obj| - check_obj(obj) - casted_by[casted_by_property.to_s] << obj.id - end - end - - def << obj - add_to_collection_with(:<<, obj) - super(obj) - end - - def push(obj) - add_to_collection_with(:push, obj) - super(obj) - end - - def unshift(obj) - add_to_collection_with(:unshift, obj) - super(obj) - end - - def []= index, obj - add_to_collection_with(:[]=, obj, index) - super(index, obj) - end - - def pop - obj = casted_by.send(casted_by_property.options[:proxy_name]).last - casted_by[casted_by_property.to_s].pop - obj.set_back_association(nil, casted_by.class.name, casted_by_property.options[:reverse_association]) - casted_by.register_dirty_association(obj) - super - end - - def shift - obj = casted_by.send(casted_by_property.options[:proxy_name]).first - casted_by[casted_by_property.to_s].shift - obj.set_back_association(nil, casted_by.class.name, casted_by_property.options[:reverse_association]) - casted_by.register_dirty_association(obj) - super - end - - protected - - def check_obj(obj) - raise "Object cannot be added to #{casted_by.class.to_s}##{casted_by_property.to_s} collection unless saved" if obj.new? - end - - def add_to_collection_with(method, obj, index=nil) - check_obj(obj) - args = [ obj.id ] - args = args.insert(0, index) if index - casted_by[casted_by_property.to_s].send(method, *args) - obj.set_back_association(casted_by, casted_by.class.name, casted_by_property.options[:reverse_association]) - casted_by.register_dirty_association(obj) - end - - # Override CastedArray instantiation_and_cast method for a simpler - # version that will not try to cast the model. - def instantiate_and_cast(obj, change = true) - couchrest_parent_will_change! if change && use_dirty? - obj.casted_by = casted_by if obj.respond_to?(:casted_by) - obj.casted_by_property = casted_by_property if obj.respond_to?(:casted_by_property) - obj - end - - end - end end diff --git a/lib/couchrest/model/associations/collection_of_proxy.rb b/lib/couchrest/model/associations/collection_of_proxy.rb new file mode 100644 index 00000000..0e772359 --- /dev/null +++ b/lib/couchrest/model/associations/collection_of_proxy.rb @@ -0,0 +1,81 @@ +module CouchRest + module Model + module Associations + # Special proxy for a collection of items so that adding and removing + # to the list automatically updates the associated property. + class CollectionOfProxy < CastedArray + + def initialize(array, property, parent) + (array ||= []).compact! + super(array, property, parent) + casted_by[casted_by_property.to_s] = [] # replace the original array! + array.compact.each do |obj| + check_obj(obj) + casted_by[casted_by_property.to_s] << obj.id + end + end + + def << obj + add_to_collection_with(:<<, obj) + super(obj) + end + + def push(obj) + add_to_collection_with(:push, obj) + super(obj) + end + + def unshift(obj) + add_to_collection_with(:unshift, obj) + super(obj) + end + + def []= index, obj + add_to_collection_with(:[]=, obj, index) + super(index, obj) + end + + def pop + obj = casted_by.send(casted_by_property.options[:proxy_name]).last + casted_by[casted_by_property.to_s].pop + obj.set_back_association(nil, casted_by.class.name, casted_by_property.options[:reverse_association]) + casted_by.register_dirty_association(obj) + super + end + + def shift + obj = casted_by.send(casted_by_property.options[:proxy_name]).first + casted_by[casted_by_property.to_s].shift + obj.set_back_association(nil, casted_by.class.name, casted_by_property.options[:reverse_association]) + casted_by.register_dirty_association(obj) + super + end + + protected + + def check_obj(obj) + raise "Object cannot be added to #{casted_by.class.to_s}##{casted_by_property.to_s} collection unless saved" if obj.new? + end + + def add_to_collection_with(method, obj, index=nil) + check_obj(obj) + args = [ obj.id ] + args = args.insert(0, index) if index + casted_by[casted_by_property.to_s].send(method, *args) + obj.set_back_association(casted_by, casted_by.class.name, casted_by_property.options[:reverse_association]) + casted_by.register_dirty_association(obj) + end + + # Override CastedArray instantiation_and_cast method for a simpler + # version that will not try to cast the model. + def instantiate_and_cast(obj, change = true) + couchrest_parent_will_change! if change && use_dirty? + obj.casted_by = casted_by if obj.respond_to?(:casted_by) + obj.casted_by_property = casted_by_property if obj.respond_to?(:casted_by_property) + obj + end + + end + end + end +end diff --git a/lib/couchrest_model.rb b/lib/couchrest_model.rb index e66078e7..08c32ab3 100644 --- a/lib/couchrest_model.rb +++ b/lib/couchrest_model.rb @@ -40,6 +40,7 @@ require "couchrest/model/extended_attachments" require "couchrest/model/proxyable" require "couchrest/model/associations" +require "couchrest/model/associations/collection_of_proxy" require "couchrest/model/configuration" require "couchrest/model/connection" require "couchrest/model/design" diff --git a/spec/unit/assocations_spec.rb b/spec/unit/assocations_spec.rb index 2582bf3d..cb2c2eb5 100644 --- a/spec/unit/assocations_spec.rb +++ b/spec/unit/assocations_spec.rb @@ -111,7 +111,7 @@ def SaleInvoice.merge_assoc_opts(*args) it "should create an associated property and collection proxy" do @invoice.respond_to?('entry_ids').should be_true @invoice.respond_to?('entry_ids=').should be_true - @invoice.entries.class.should eql(::CouchRest::Model::CollectionOfProxy) + @invoice.entries.class.should eql(::CouchRest::Model::Associations::CollectionOfProxy) end it "should allow replacement of objects" do From 12b54bea847c1a3c6b6631e342cdb9334e86ed6d Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Fri, 13 Mar 2015 22:47:53 +0000 Subject: [PATCH 17/21] [back_assignments] add pet fixctures model --- spec/fixtures/models/pet.rb | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 spec/fixtures/models/pet.rb diff --git a/spec/fixtures/models/pet.rb b/spec/fixtures/models/pet.rb new file mode 100644 index 00000000..7f395f23 --- /dev/null +++ b/spec/fixtures/models/pet.rb @@ -0,0 +1,6 @@ +class Pet < CouchRest::Model::Base + property :name, String + + belongs_to :walker, :class_name => 'Parent' + belongs_to :owner, :class_name => 'Parent' +end From 3dbabb29c24ca512b1d35cde4f06ea8382c735a7 Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Sun, 15 Mar 2015 23:28:26 +0000 Subject: [PATCH 18/21] add documentation for revers_associations --- lib/couchrest/model/associations.rb | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/lib/couchrest/model/associations.rb b/lib/couchrest/model/associations.rb index 02a7885c..18d15643 100644 --- a/lib/couchrest/model/associations.rb +++ b/lib/couchrest/model/associations.rb @@ -18,8 +18,8 @@ module ClassMethods # An attribute will be created matching the name of the attribute # with '_id' on the end, or the foreign key (:foreign_key) provided. # - # Searching for the assocated object is performed using a string - # (:proxy) to be evaulated in the context of the owner. Typically + # Searching for the associated object is performed using a string + # (:proxy) to be evaluated in the context of the owner. Typically # this will be set to the class name (:class_name), or determined # automatically if the owner belongs to a proxy object. # @@ -35,9 +35,16 @@ module ClassMethods # # self.company.clients # - # If the name of the collection proxy is not the pluralized assocation name, + # If the name of the collection proxy is not the pluralized association name, # it can be set with the :proxy_name option. # + # If the owner model define an association back to the belonged model, setting + # the owner will also set the (:reverse_association) attribute of the owner. + # After such affectation, saving the object model will also trigger the save of + # the owner object. + # (:reverse_association) is optional and should be used only to remove ambiguity, + # when it can't be calculated from the (:class_name) + # def belongs_to(attrib, *options) opts = merge_belongs_to_association_options(attrib, options.first) @@ -89,6 +96,14 @@ def belongs_to(attrib, *options) # NOTE: This method is *not* recommended for large collections or collections that change # frequently! Use with prudence. # + # If the associated model define an association back to the collection owner model, adding + # or removing from the collection will also populate the (:reverse_association) attribute + # of associated model. + # After such affectation, saving the object model will also trigger the save of + # the associated object. + # (:reverse_association) is optional and should be used only to remove ambiguity, + # when it can't be calculated from the (:class_name) + # def collection_of(attrib, *options) opts = merge_belongs_to_association_options(attrib, options.first) opts[:foreign_key] = opts[:foreign_key].pluralize From 8274b9d2427954f1e58629a7f35c0960a61240c0 Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Fri, 10 Apr 2015 22:38:49 +0200 Subject: [PATCH 19/21] [back_assignments] activate back assignement only if the reverse_association is specified. --- lib/couchrest/model/associations.rb | 14 +--- spec/fixtures/models/kid.rb | 4 +- spec/unit/assocations_dual_spec.rb | 102 ++++++++++++++++------------ 3 files changed, 62 insertions(+), 58 deletions(-) diff --git a/lib/couchrest/model/associations.rb b/lib/couchrest/model/associations.rb index 18d15643..ecacf590 100644 --- a/lib/couchrest/model/associations.rb +++ b/lib/couchrest/model/associations.rb @@ -212,21 +212,13 @@ def #{attrib}=(value) def set_back_association(value, class_name, reverse_association = nil) if reverse_association && !reverse_association.empty? - prop = self.class.properties.detect { |prop| prop.name =~ %r{#{reverse_association}_id} } - if prop.type.ancestors.include? Enumerable + prop = self.class.properties.detect { |prop| prop.name =~ %r{#{reverse_association.to_s.singularize}_ids?} } + raise "Cannot find reverse association: #{reverse_association}" unless prop + if attributes[prop.name].class.ancestors.include?(Enumerable) instance_eval("#{prop.name}.push('#{value.nil? ? nil : value.id}')") else send("#{prop.name}=", (value.nil? ? nil : value.id)) end - else - assoc = self.class.associations.detect { |ass| ass[:options][:class_name] == class_name } - return unless assoc - case assoc[:type] - when :belongs_to - send("#{assoc[:options][:foreign_key]}=", (value.nil? ? nil : value.id)) - when :collection_of - instance_eval("#{assoc[:options][:foreign_key]}.push('#{value.nil? ? nil : value.id}')") - end end end diff --git a/spec/fixtures/models/kid.rb b/spec/fixtures/models/kid.rb index 518ef2d5..fd9b73c1 100644 --- a/spec/fixtures/models/kid.rb +++ b/spec/fixtures/models/kid.rb @@ -1,7 +1,7 @@ class Kid < CouchRest::Model::Base property :name, String - belongs_to :dad, :class_name => 'Parent' - belongs_to :mum, :class_name => 'Parent' + belongs_to :dad, :class_name => 'Parent', :reverse_association => :children + belongs_to :mum, :class_name => 'Parent', :reverse_association => :children end diff --git a/spec/unit/assocations_dual_spec.rb b/spec/unit/assocations_dual_spec.rb index 89657f79..9e9b16bf 100644 --- a/spec/unit/assocations_dual_spec.rb +++ b/spec/unit/assocations_dual_spec.rb @@ -11,10 +11,10 @@ describe 'of type belongs_to' do context 'with the other side also belongs_to (1-1)' do - context '[non ambiguous association]' do - it 'should set the other side property too' do + context 'when reverse association not specified' do + it 'should NOT set the other side' do father.super_power = can_fly - can_fly.parent.should eql father + can_fly.parent.should be_nil end end @@ -69,77 +69,89 @@ describe 'of type collection_of' do context 'with the other side is a belongs_to (n-1).' do context 'Adding to the collection using <<' do - it 'should populate the belongs_to property' do - father.children << kid - kid.dad.should eq(father) + context 'when NO reverse_assocition is specified' do + it 'should populate the belongs_to property' do + father.children << kid + kid.dad.should be_nil + end end context 'when reverse_association is specified' do it 'should populate the belongs_to property' do father.pets << dog dog.owner.should eq(father) end - end - describe 'when object is saved' do - it 'should also save other side' do - father.children << kid - kid.should_receive(:save) - father.save + describe 'when object is saved' do + it 'should also save other side' do + father.children << kid + kid.should_receive(:save) + father.save + end end end end context 'Adding to the collection using push' do - it 'should populate the belongs_to property' do - father.children.push kid - kid.dad.should eq(father) + context 'when NO reverse_assocition is specified' do + it 'should populate the belongs_to property' do + father.children.push kid + kid.dad.should be_nil + end end context 'when reverse_association is specified' do it 'should populate the belongs_to property' do father.pets.push dog dog.owner.should eq(father) end - end - describe 'when object is saved' do - it 'should also save other side' do - father.children.push kid - kid.should_receive(:save) - father.save + describe 'when object is saved' do + it 'should also save other side' do + father.children.push kid + kid.should_receive(:save) + father.save + end end end end context 'Adding to the collection using unshift' do - it 'should populate the belongs_to property' do - father.children.unshift kid - kid.dad.should eq(father) - end - it 'should populate the belongs_to property' do - father.pets.unshift dog - dog.owner.should eq(father) - end - describe 'when object is saved' do - it 'should also save other side' do + context 'when NO reverse_assocition is specified' do + it 'should NOT populate the belongs_to property' do father.children.unshift kid - kid.should_receive(:save) - father.save + kid.dad.should be_nil + end + end + context 'when a reverse_assocition is specified' do + it 'should populate the belongs_to property' do + father.pets.unshift dog + dog.owner.should eq(father) + end + describe 'when object is saved' do + it 'should also save other side' do + father.pets.unshift dog + dog.should_receive(:save) + father.save + end end end end context 'Adding to the collection using [n]=' do - it 'should populate the belongs_to property' do - father.children[3]= kid - kid.dad.should eq(father) - end - it 'should populate the belongs_to property' do - father.pets[3] = dog - dog.owner.should eq(father) + context 'when NO reverse_assocition is specified' do + it 'should NOT populate the belongs_to property' do + father.children[3]= kid + kid.dad.should be_nil + end end - describe 'when object is saved' do - it 'should also save other side' do - father.children[4] = kid - kid.should_receive(:save) - father.save + context 'when a reverse_assocition is specified' do + it 'should populate the belongs_to property' do + father.pets[3] = dog + dog.owner.should eq(father) + end + describe 'when object is saved' do + it 'should also save other side' do + father.pets[4] = dog + dog.should_receive(:save) + father.save + end end end end From bd6c4dae79e4447ca9334b84b98008b4b25d31ba Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Fri, 10 Apr 2015 22:54:12 +0200 Subject: [PATCH 20/21] [back_assignments] update README --- README.md | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 590cdb1b..94c9bc4d 100644 --- a/README.md +++ b/README.md @@ -107,7 +107,7 @@ $ rails generate couchrest_model:config $ rails generate model person --orm=couchrest_model ``` -## General Usage +## General Usage ```ruby require 'couchrest_model' @@ -140,6 +140,20 @@ end @cat.update_attributes(:name => 'Felix', :random_text => 'feline') @cat.new? # false @cat.random_text # Raises error! + +### reverse associations +class Parent < CouchRest::Model::Base + collection_of :children +end + +class Child < CouchRest::Model::Base + belongs_to :dad, class: Parent, :reverse_association => :children +end + +@bob = Parent.new +@kevin = Child.new +@kevin.dad = @bob +@bob.children.include?(@kevin) # true ``` ## Development From de2bfc3c06149728bb5ae3a55044acf209729f6a Mon Sep 17 00:00:00 2001 From: yann ARMAND Date: Fri, 10 Apr 2015 23:04:17 +0200 Subject: [PATCH 21/21] [back_assignments] update documentation. --- lib/couchrest/model/associations.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/couchrest/model/associations.rb b/lib/couchrest/model/associations.rb index ecacf590..9170e51c 100644 --- a/lib/couchrest/model/associations.rb +++ b/lib/couchrest/model/associations.rb @@ -42,8 +42,8 @@ module ClassMethods # the owner will also set the (:reverse_association) attribute of the owner. # After such affectation, saving the object model will also trigger the save of # the owner object. - # (:reverse_association) is optional and should be used only to remove ambiguity, - # when it can't be calculated from the (:class_name) + # (:reverse_association) is optional. When used, saving the belonged object will + # trigger the save of the owner object. # def belongs_to(attrib, *options) opts = merge_belongs_to_association_options(attrib, options.first) @@ -101,8 +101,8 @@ def belongs_to(attrib, *options) # of associated model. # After such affectation, saving the object model will also trigger the save of # the associated object. - # (:reverse_association) is optional and should be used only to remove ambiguity, - # when it can't be calculated from the (:class_name) + # (:reverse_association) is optional. When used, saving the object with a collection will + # trigger save of the new members of the collection. # def collection_of(attrib, *options) opts = merge_belongs_to_association_options(attrib, options.first)