adding support for collection_of association
This commit is contained in:
parent
fa0ab968a8
commit
dd55466764
4 changed files with 281 additions and 11 deletions
|
@ -16,6 +16,7 @@
|
|||
* Major enhancements
|
||||
* Added support for anonymous CastedModels defined in Documents
|
||||
* Added initial support for simple belongs_to associations
|
||||
* Added support for basic collection_of association (unique to document databases!)
|
||||
|
||||
== 1.0.0.beta5
|
||||
|
||||
|
|
|
@ -2,19 +2,18 @@ module CouchRest
|
|||
module CastedModel
|
||||
|
||||
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::Properties)
|
||||
base.send(:include, ::CouchRest::Mixins::Assocations)
|
||||
base.send(:include, ::CouchRest::Mixins::Associations)
|
||||
base.send(:attr_accessor, :casted_by)
|
||||
end
|
||||
|
||||
def initialize(keys={})
|
||||
def initialize(keys = {})
|
||||
raise StandardError unless self.is_a? Hash
|
||||
apply_all_property_defaults # defined in CouchRest::Mixins::Properties
|
||||
prepare_all_attributes(keys)
|
||||
super()
|
||||
keys.each do |k,v|
|
||||
write_attribute(k.to_s, v)
|
||||
end if keys
|
||||
end
|
||||
|
||||
def []= key, value
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
module CouchRest
|
||||
|
||||
|
||||
module Mixins
|
||||
module Associations
|
||||
|
||||
|
@ -10,10 +12,13 @@ module CouchRest
|
|||
|
||||
module ClassMethods
|
||||
|
||||
# Define an association that this object belongs to.
|
||||
#
|
||||
def belongs_to(attrib, *options)
|
||||
opts = {
|
||||
:foreign_key => attrib.to_s + '_id',
|
||||
:class_name => attrib.to_s.camelcase
|
||||
:class_name => attrib.to_s.camelcase,
|
||||
:proxy => nil
|
||||
}
|
||||
case options.first
|
||||
when Hash
|
||||
|
@ -23,10 +28,10 @@ module CouchRest
|
|||
begin
|
||||
opts[:class] = opts[:class_name].constantize
|
||||
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
|
||||
|
||||
prop = property(opts[:foreign_key])
|
||||
prop = property(opts[:foreign_key], opts)
|
||||
|
||||
create_belongs_to_getter(attrib, prop, opts)
|
||||
create_belongs_to_setter(attrib, prop, opts)
|
||||
|
@ -34,10 +39,76 @@ module CouchRest
|
|||
prop
|
||||
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)
|
||||
base = options[:proxy] || options[:class_name]
|
||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def #{attrib}
|
||||
@#{attrib} ||= #{options[:class_name]}.get(self.#{options[:foreign_key]})
|
||||
@#{attrib} ||= #{base}.get(self.#{options[:foreign_key]})
|
||||
end
|
||||
EOS
|
||||
end
|
||||
|
@ -45,14 +116,92 @@ module CouchRest
|
|||
def create_belongs_to_setter(attrib, property, options)
|
||||
class_eval <<-EOS, __FILE__, __LINE__ + 1
|
||||
def #{attrib}=(value)
|
||||
@#{attrib} = value
|
||||
self.#{options[:foreign_key]} = value.nil? ? nil : value.id
|
||||
@#{attrib} = value
|
||||
end
|
||||
EOS
|
||||
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
|
||||
|
||||
# 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
|
||||
|
|
|
@ -8,12 +8,21 @@ class Client < CouchRest::ExtendedDocument
|
|||
property :tax_code
|
||||
end
|
||||
|
||||
class SaleEntry < CouchRest::ExtendedDocument
|
||||
use_database DB
|
||||
|
||||
property :description
|
||||
property :price
|
||||
end
|
||||
|
||||
class SaleInvoice < CouchRest::ExtendedDocument
|
||||
use_database DB
|
||||
|
||||
belongs_to :client
|
||||
belongs_to :alternate_client, :class_name => 'Client', :foreign_key => 'alt_client_id'
|
||||
|
||||
collection_of :entries, :class_name => 'SaleEntry'
|
||||
|
||||
property :date, Date
|
||||
property :price, Integer
|
||||
end
|
||||
|
@ -76,5 +85,117 @@ describe "Assocations" do
|
|||
|
||||
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
|
||||
|
||||
|
|
Loading…
Reference in a new issue