adding support for collection_of association
This commit is contained in:
parent
fa0ab968a8
commit
dd55466764
|
@ -16,6 +16,7 @@
|
||||||
* Major enhancements
|
* Major enhancements
|
||||||
* Added support for anonymous CastedModels defined in Documents
|
* Added support for anonymous CastedModels defined in Documents
|
||||||
* Added initial support for simple belongs_to associations
|
* Added initial support for simple belongs_to associations
|
||||||
|
* Added support for basic collection_of association (unique to document databases!)
|
||||||
|
|
||||||
== 1.0.0.beta5
|
== 1.0.0.beta5
|
||||||
|
|
||||||
|
|
|
@ -2,19 +2,18 @@ module CouchRest
|
||||||
module CastedModel
|
module CastedModel
|
||||||
|
|
||||||
def self.included(base)
|
def self.included(base)
|
||||||
|
base.send(:include, ::CouchRest::Mixins::AttributeProtection)
|
||||||
|
base.send(:include, ::CouchRest::Mixins::Attributes)
|
||||||
base.send(:include, ::CouchRest::Mixins::Callbacks)
|
base.send(:include, ::CouchRest::Mixins::Callbacks)
|
||||||
base.send(:include, ::CouchRest::Mixins::Properties)
|
base.send(:include, ::CouchRest::Mixins::Properties)
|
||||||
base.send(:include, ::CouchRest::Mixins::Assocations)
|
base.send(:include, ::CouchRest::Mixins::Associations)
|
||||||
base.send(:attr_accessor, :casted_by)
|
base.send(:attr_accessor, :casted_by)
|
||||||
end
|
end
|
||||||
|
|
||||||
def initialize(keys={})
|
def initialize(keys = {})
|
||||||
raise StandardError unless self.is_a? Hash
|
raise StandardError unless self.is_a? Hash
|
||||||
apply_all_property_defaults # defined in CouchRest::Mixins::Properties
|
prepare_all_attributes(keys)
|
||||||
super()
|
super()
|
||||||
keys.each do |k,v|
|
|
||||||
write_attribute(k.to_s, v)
|
|
||||||
end if keys
|
|
||||||
end
|
end
|
||||||
|
|
||||||
def []= key, value
|
def []= key, value
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
module CouchRest
|
module CouchRest
|
||||||
|
|
||||||
|
|
||||||
module Mixins
|
module Mixins
|
||||||
module Associations
|
module Associations
|
||||||
|
|
||||||
|
@ -10,10 +12,13 @@ module CouchRest
|
||||||
|
|
||||||
module ClassMethods
|
module ClassMethods
|
||||||
|
|
||||||
|
# Define an association that this object belongs to.
|
||||||
|
#
|
||||||
def belongs_to(attrib, *options)
|
def belongs_to(attrib, *options)
|
||||||
opts = {
|
opts = {
|
||||||
:foreign_key => attrib.to_s + '_id',
|
:foreign_key => attrib.to_s + '_id',
|
||||||
:class_name => attrib.to_s.camelcase
|
:class_name => attrib.to_s.camelcase,
|
||||||
|
:proxy => nil
|
||||||
}
|
}
|
||||||
case options.first
|
case options.first
|
||||||
when Hash
|
when Hash
|
||||||
|
@ -23,10 +28,10 @@ module CouchRest
|
||||||
begin
|
begin
|
||||||
opts[:class] = opts[:class_name].constantize
|
opts[:class] = opts[:class_name].constantize
|
||||||
rescue
|
rescue
|
||||||
raise "Unable to convert belongs_to class name into Constant for #{self.name}##{attrib}"
|
raise "Unable to convert class name into Constant for #{self.name}##{attrib}"
|
||||||
end
|
end
|
||||||
|
|
||||||
prop = property(opts[:foreign_key])
|
prop = property(opts[:foreign_key], opts)
|
||||||
|
|
||||||
create_belongs_to_getter(attrib, prop, opts)
|
create_belongs_to_getter(attrib, prop, opts)
|
||||||
create_belongs_to_setter(attrib, prop, opts)
|
create_belongs_to_setter(attrib, prop, opts)
|
||||||
|
@ -34,10 +39,76 @@ module CouchRest
|
||||||
prop
|
prop
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Provide access to a collection of objects where the associated
|
||||||
|
# property contains a list of the collection item ids.
|
||||||
|
#
|
||||||
|
# The following:
|
||||||
|
#
|
||||||
|
# collection_of :groups
|
||||||
|
#
|
||||||
|
# creates a pseudo property called "groups" which allows access
|
||||||
|
# to a CollectionProxy object. Adding, replacing or removing entries in this
|
||||||
|
# proxy will cause the matching property array, in this case "group_ids", to
|
||||||
|
# be kept in sync.
|
||||||
|
#
|
||||||
|
# Any manual changes made to the collection ids property (group_ids), unless replaced, will require
|
||||||
|
# a reload of the CollectionProxy for the two sets of data to be in sync:
|
||||||
|
#
|
||||||
|
# group_ids = ['123']
|
||||||
|
# groups == [Group.get('123')]
|
||||||
|
# group_ids << '321'
|
||||||
|
# groups == [Group.get('123')]
|
||||||
|
# groups(true) == [Group.get('123'), Group.get('321')]
|
||||||
|
#
|
||||||
|
# Of course, saving the parent record will store the collection ids as they are
|
||||||
|
# found.
|
||||||
|
#
|
||||||
|
# The CollectionProxy supports the following array functions, anything else will cause
|
||||||
|
# a mismatch between the collection objects and collection ids:
|
||||||
|
#
|
||||||
|
# groups << obj
|
||||||
|
# groups.push obj
|
||||||
|
# groups.unshift obj
|
||||||
|
# groups[0] = obj
|
||||||
|
# groups.pop == obj
|
||||||
|
# groups.shift == obj
|
||||||
|
#
|
||||||
|
# Addtional options match those of the the belongs_to method.
|
||||||
|
#
|
||||||
|
def collection_of(attrib, *options)
|
||||||
|
opts = {
|
||||||
|
:foreign_key => attrib.to_s.singularize + '_ids',
|
||||||
|
:class_name => attrib.to_s.singularize.camelcase,
|
||||||
|
:proxy => nil
|
||||||
|
}
|
||||||
|
case options.first
|
||||||
|
when Hash
|
||||||
|
opts.merge!(options.first)
|
||||||
|
end
|
||||||
|
begin
|
||||||
|
opts[:class] = opts[:class_name].constantize
|
||||||
|
rescue
|
||||||
|
raise "Unable to convert class name into Constant for #{self.name}##{attrib}"
|
||||||
|
end
|
||||||
|
opts[:readonly] = true
|
||||||
|
|
||||||
|
prop = property(opts[:foreign_key], [], opts)
|
||||||
|
|
||||||
|
create_collection_of_property_setter(attrib, prop, opts)
|
||||||
|
create_collection_of_getter(attrib, prop, opts)
|
||||||
|
create_collection_of_setter(attrib, prop, opts)
|
||||||
|
|
||||||
|
prop
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
def create_belongs_to_getter(attrib, property, options)
|
def create_belongs_to_getter(attrib, property, options)
|
||||||
|
base = options[:proxy] || options[:class_name]
|
||||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||||
def #{attrib}
|
def #{attrib}
|
||||||
@#{attrib} ||= #{options[:class_name]}.get(self.#{options[:foreign_key]})
|
@#{attrib} ||= #{base}.get(self.#{options[:foreign_key]})
|
||||||
end
|
end
|
||||||
EOS
|
EOS
|
||||||
end
|
end
|
||||||
|
@ -45,14 +116,92 @@ module CouchRest
|
||||||
def create_belongs_to_setter(attrib, property, options)
|
def create_belongs_to_setter(attrib, property, options)
|
||||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||||
def #{attrib}=(value)
|
def #{attrib}=(value)
|
||||||
@#{attrib} = value
|
|
||||||
self.#{options[:foreign_key]} = value.nil? ? nil : value.id
|
self.#{options[:foreign_key]} = value.nil? ? nil : value.id
|
||||||
|
@#{attrib} = value
|
||||||
end
|
end
|
||||||
EOS
|
EOS
|
||||||
end
|
end
|
||||||
|
|
||||||
|
### collection_of support methods
|
||||||
|
|
||||||
|
def create_collection_of_property_setter(attrib, property, options)
|
||||||
|
# ensure CollectionProxy is nil, ready to be reloaded on request
|
||||||
|
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||||
|
def #{options[:foreign_key]}=(value)
|
||||||
|
@#{attrib} = nil
|
||||||
|
write_attribute("#{options[:foreign_key]}", value)
|
||||||
|
end
|
||||||
|
EOS
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_collection_of_getter(attrib, property, options)
|
||||||
|
base = options[:proxy] || options[:class_name]
|
||||||
|
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||||
|
def #{attrib}(reload = false)
|
||||||
|
return @#{attrib} unless @#{attrib}.nil? or reload
|
||||||
|
ary = self.#{options[:foreign_key]}.collect{|i| #{base}.get(i)}
|
||||||
|
@#{attrib} = ::CouchRest::CollectionProxy.new(ary, self, '#{options[:foreign_key]}')
|
||||||
|
end
|
||||||
|
EOS
|
||||||
|
end
|
||||||
|
|
||||||
|
def create_collection_of_setter(attrib, property, options)
|
||||||
|
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||||
|
def #{attrib}=(value)
|
||||||
|
@#{attrib} = ::CouchRest::CollectionProxy.new(value, self, '#{options[:foreign_key]}')
|
||||||
|
end
|
||||||
|
EOS
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Special proxy for a collection of items so that adding and removing
|
||||||
|
# to the list automatically updates the associated property.
|
||||||
|
class CollectionProxy < Array
|
||||||
|
attr_accessor :property
|
||||||
|
attr_accessor :casted_by
|
||||||
|
|
||||||
|
def initialize(array, casted_by, property)
|
||||||
|
self.property = property
|
||||||
|
self.casted_by = casted_by
|
||||||
|
array ||= []
|
||||||
|
casted_by[property.to_s] = [] # replace the original array!
|
||||||
|
array.each do |obj|
|
||||||
|
casted_by[property.to_s] << obj.id
|
||||||
|
end
|
||||||
|
super(array)
|
||||||
|
end
|
||||||
|
def << obj
|
||||||
|
casted_by[property.to_s] << obj.id
|
||||||
|
super(obj)
|
||||||
|
end
|
||||||
|
def push(obj)
|
||||||
|
casted_by[property.to_s].push obj.id
|
||||||
|
super(obj)
|
||||||
|
end
|
||||||
|
def unshift(obj)
|
||||||
|
casted_by[property.to_s].unshift obj.id
|
||||||
|
super(obj)
|
||||||
|
end
|
||||||
|
|
||||||
|
def []= index, obj
|
||||||
|
casted_by[property.to_s][index] = obj.id
|
||||||
|
super(index, obj)
|
||||||
|
end
|
||||||
|
|
||||||
|
def pop
|
||||||
|
casted_by[property.to_s].pop
|
||||||
|
super
|
||||||
|
end
|
||||||
|
def shift
|
||||||
|
casted_by[property.to_s].shift
|
||||||
|
super
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
|
@ -8,12 +8,21 @@ class Client < CouchRest::ExtendedDocument
|
||||||
property :tax_code
|
property :tax_code
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class SaleEntry < CouchRest::ExtendedDocument
|
||||||
|
use_database DB
|
||||||
|
|
||||||
|
property :description
|
||||||
|
property :price
|
||||||
|
end
|
||||||
|
|
||||||
class SaleInvoice < CouchRest::ExtendedDocument
|
class SaleInvoice < CouchRest::ExtendedDocument
|
||||||
use_database DB
|
use_database DB
|
||||||
|
|
||||||
belongs_to :client
|
belongs_to :client
|
||||||
belongs_to :alternate_client, :class_name => 'Client', :foreign_key => 'alt_client_id'
|
belongs_to :alternate_client, :class_name => 'Client', :foreign_key => 'alt_client_id'
|
||||||
|
|
||||||
|
collection_of :entries, :class_name => 'SaleEntry'
|
||||||
|
|
||||||
property :date, Date
|
property :date, Date
|
||||||
property :price, Integer
|
property :price, Integer
|
||||||
end
|
end
|
||||||
|
@ -76,5 +85,117 @@ describe "Assocations" do
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe "of type collection_of" do
|
||||||
|
|
||||||
|
before(:each) do
|
||||||
|
@invoice = SaleInvoice.create(:price => "sam", :price => 2000)
|
||||||
|
@entries = [
|
||||||
|
SaleEntry.create(:description => 'test line 1', :price => 500),
|
||||||
|
SaleEntry.create(:description => 'test line 2', :price => 500),
|
||||||
|
SaleEntry.create(:description => 'test line 3', :price => 1000)
|
||||||
|
]
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should create an associated property and collection proxy" do
|
||||||
|
@invoice.respond_to?('entry_ids')
|
||||||
|
@invoice.respond_to?('entry_ids=')
|
||||||
|
@invoice.entries.class.should eql(::CouchRest::CollectionProxy)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should allow replacement of objects" do
|
||||||
|
@invoice.entries = @entries
|
||||||
|
@invoice.entries.length.should eql(3)
|
||||||
|
@invoice.entry_ids.length.should eql(3)
|
||||||
|
@invoice.entries.first.should eql(@entries.first)
|
||||||
|
@invoice.entry_ids.first.should eql(@entries.first.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should allow ids to be set directly and load entries" do
|
||||||
|
@invoice.entry_ids = @entries.collect{|i| i.id}
|
||||||
|
@invoice.entries.length.should eql(3)
|
||||||
|
@invoice.entries.first.should eql(@entries.first)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should replace collection if ids replaced" do
|
||||||
|
@invoice.entry_ids = @entries.collect{|i| i.id}
|
||||||
|
@invoice.entries.length.should eql(3) # load once
|
||||||
|
@invoice.entry_ids = @entries[0..1].collect{|i| i.id}
|
||||||
|
@invoice.entries.length.should eql(2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should allow forced collection update if ids changed" do
|
||||||
|
@invoice.entry_ids = @entries[0..1].collect{|i| i.id}
|
||||||
|
@invoice.entries.length.should eql(2) # load once
|
||||||
|
@invoice.entry_ids << @entries[2].id
|
||||||
|
@invoice.entry_ids.length.should eql(3)
|
||||||
|
@invoice.entries.length.should eql(2) # cached!
|
||||||
|
@invoice.entries(true).length.should eql(3)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should empty arrays when nil collection provided" do
|
||||||
|
@invoice.entries = @entries
|
||||||
|
@invoice.entries = nil
|
||||||
|
@invoice.entry_ids.should be_empty
|
||||||
|
@invoice.entries.should be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should empty arrays when nil ids array provided" do
|
||||||
|
@invoice.entries = @entries
|
||||||
|
@invoice.entry_ids = nil
|
||||||
|
@invoice.entry_ids.should be_empty
|
||||||
|
@invoice.entries.should be_empty
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "proxy" do
|
||||||
|
|
||||||
|
it "should ensure new entries to proxy are matched" do
|
||||||
|
@invoice.entries << @entries.first
|
||||||
|
@invoice.entry_ids.first.should eql(@entries.first.id)
|
||||||
|
@invoice.entries.first.should eql(@entries.first)
|
||||||
|
@invoice.entries << @entries[1]
|
||||||
|
@invoice.entries.count.should eql(2)
|
||||||
|
@invoice.entry_ids.count.should eql(2)
|
||||||
|
@invoice.entry_ids.last.should eql(@entries[1].id)
|
||||||
|
@invoice.entries.last.should eql(@entries[1])
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should support push method" do
|
||||||
|
@invoice.entries.push(@entries.first)
|
||||||
|
@invoice.entry_ids.first.should eql(@entries.first.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should support []= method" do
|
||||||
|
@invoice.entries[0] = @entries.first
|
||||||
|
@invoice.entry_ids.first.should eql(@entries.first.id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should support unshift method" do
|
||||||
|
@invoice.entries.unshift(@entries.first)
|
||||||
|
@invoice.entry_ids.first.should eql(@entries.first.id)
|
||||||
|
@invoice.entries.unshift(@entries[1])
|
||||||
|
@invoice.entry_ids.first.should eql(@entries[1].id)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should support pop method" do
|
||||||
|
@invoice.entries.push(@entries.first)
|
||||||
|
@invoice.entries.pop.should eql(@entries.first)
|
||||||
|
@invoice.entries.empty?.should be_true
|
||||||
|
@invoice.entry_ids.empty?.should be_true
|
||||||
|
end
|
||||||
|
|
||||||
|
it "should support shift method" do
|
||||||
|
@invoice.entries.push(@entries[0])
|
||||||
|
@invoice.entries.push(@entries[1])
|
||||||
|
@invoice.entries.shift.should eql(@entries[0])
|
||||||
|
@invoice.entries.first.should eql(@entries[1])
|
||||||
|
@invoice.entry_ids.first.should eql(@entries[1].id)
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
|
|
||||||
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue