working on proxy association and handling

This commit is contained in:
Sam Lown 2011-04-04 01:10:31 +02:00
parent 363461fc9d
commit 8fa7e87019
5 changed files with 75 additions and 67 deletions

View file

@ -11,23 +11,33 @@ module CouchRest
module ClassMethods module ClassMethods
# Define an association that this object belongs to. # Define an association that this object belongs to.
# #
# 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
# 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
# association inside owner's owner, or proxy.
#
# For example, imagine a company acts as a proxy for invoices and clients.
# If an invoice belongs to a client, the invoice will need to access the
# list of clients via the proxy. So a request to search for the associated
# client from an invoice would look like:
#
# self.company.clients
#
# 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) def belongs_to(attrib, *options)
opts = {
:foreign_key => attrib.to_s + '_id',
:class_name => attrib.to_s.camelcase,
:proxy => nil
}
case options.first
when Hash
opts.merge!(options.first)
end
begin opts = merge_belongs_to_assocation_options(attrib, options.first)
opts[:class] = opts[:class_name].constantize
rescue NameError
raise NameError, "Unable to convert class name into Constant for #{self.name}##{attrib}"
end
prop = property(opts[:foreign_key], opts) prop = property(opts[:foreign_key], opts)
@ -77,20 +87,9 @@ module CouchRest
# frequently! Use with prudence. # frequently! Use with prudence.
# #
def collection_of(attrib, *options) def collection_of(attrib, *options)
opts = {
:foreign_key => attrib.to_s.singularize + '_ids', opts = merge_belongs_to_assocation_options(attrib, options.first)
:class_name => attrib.to_s.singularize.camelcase, opts[:foreign_key] = opts[:foreign_key].pluralize
: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 opts[:readonly] = true
prop = property(opts[:foreign_key], [], opts) prop = property(opts[:foreign_key], [], opts)
@ -105,11 +104,42 @@ module CouchRest
private private
def merge_belongs_to_assocation_options(attrib, options = nil)
opts = {
:foreign_key => attrib.to_s + '_id',
:class_name => attrib.to_s.camelcase,
:proxy_name => attrib.to_s.pluralize
}
# merge the options
case options
when Hash
opts.merge!(options)
end
# Get a class name
begin
opts[:class] = opts[:class_name].constantize
rescue NameError
raise NameError, "Unable to convert class name into Constant for #{self.name}##{attrib}"
end
# Generate a string for the proxy method call
# Assumes that the proxy_owner_method from "proxyable" is available.
if opts[:proxy].to_s.empty?
opts[:proxy] = if proxy_owner_method
"self.#{proxy_owner_method}.#{opts[:proxy_name]}"
else
opts[:class_name].constantize
end
end
opts
end
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[:foreign_key]}.nil? ? nil : (model_proxy || #{base}).get(self.#{options[:foreign_key]}) @#{attrib} ||= #{options[:foreign_key]}.nil? ? nil : #{options[:proxy]}.get(self.#{options[:foreign_key]})
end end
EOS EOS
end end
@ -140,7 +170,7 @@ module CouchRest
class_eval <<-EOS, __FILE__, __LINE__ + 1 class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{attrib}(reload = false) def #{attrib}(reload = false)
return @#{attrib} unless @#{attrib}.nil? or reload return @#{attrib} unless @#{attrib}.nil? or reload
ary = self.#{options[:foreign_key]}.collect{|i| (model_proxy || #{base}).get(i)} ary = self.#{options[:foreign_key]}.collect{|i| #{base}.get(i)}
@#{attrib} = ::CouchRest::CollectionOfProxy.new(ary, self, '#{options[:foreign_key]}') @#{attrib} = ::CouchRest::CollectionOfProxy.new(ary, self, '#{options[:foreign_key]}')
end end
EOS EOS

View file

@ -54,13 +54,6 @@ module CouchRest::Model
alias :to_key :id alias :to_key :id
alias :to_param :id alias :to_param :id
# Fixes belongs_to assocations.
# See it "should allow to reference association by id in casted model" in
# assocations_spec.rb
def model_proxy
nil
end
# Sets the attributes from a hash # Sets the attributes from a hash
def update_attributes_without_saving(hash) def update_attributes_without_saving(hash)
hash.each do |k, v| hash.each do |k, v|

View file

@ -4,29 +4,29 @@ module CouchRest
module Proxyable module Proxyable
extend ActiveSupport::Concern extend ActiveSupport::Concern
included do
attr_accessor :model_proxy
end
module ClassMethods module ClassMethods
attr_reader :proxy_owner_method
# Define a collection that will use the base model for the database connection # Define a collection that will use the base model for the database connection
# details. # details.
def proxy_for(model_name, options = {}) def proxy_for(assoc_name, options = {})
db_method = options[:database_method] || "proxy_database" db_method = options[:database_method] || "proxy_database"
options[:class_name] ||= model_name.to_s.singularize.camelize options[:class_name] ||= assoc_name.to_s.singularize.camelize
attr_accessor :model_proxy
class_eval <<-EOS, __FILE__, __LINE__ + 1 class_eval <<-EOS, __FILE__, __LINE__ + 1
def #{model_name} def #{assoc_name}
unless respond_to?('#{db_method}') unless respond_to?('#{db_method}')
raise "Missing ##{db_method} method for proxy" raise "Missing ##{db_method} method for proxy"
end end
@#{model_name} ||= CouchRest::Model::Proxyable::ModelProxy.new(::#{options[:class_name]}, self, self.class.to_s.underscore, #{db_method}) @#{assoc_name} ||= CouchRest::Model::Proxyable::ModelProxy.new(::#{options[:class_name]}, self, self.class.to_s.underscore, #{db_method})
end end
EOS EOS
end end
def proxied_by(model_name, options = {}) def proxied_by(model_name, options = {})
raise "Model can only be proxied once or ##{model_name} already defined" if method_defined?(model_name) raise "Model can only be proxied once or ##{model_name} already defined" if method_defined?(model_name) || !proxy_owner_method.nil?
self.proxy_owner_method = model_name
attr_accessor model_name attr_accessor model_name
end end
end end
@ -134,7 +134,7 @@ module CouchRest
def proxy_update(doc) def proxy_update(doc)
if doc if doc
doc.database = @database if doc.respond_to?(:database=) doc.database = @database if doc.respond_to?(:database=)
doc.model_proxy = self if doc.respond_to?(:model_proxy=) doc.model_proxy = self if doc.respond_to?(:model_proxy=)
doc.send("#{owner_name}=", owner) if doc.respond_to?("#{owner_name}=") doc.send("#{owner_name}=", owner) if doc.respond_to?("#{owner_name}=")
end end
doc doc

View file

@ -15,7 +15,8 @@ module CouchRest
def validate_each(document, attribute, value) def validate_each(document, attribute, value)
view_name = options[:view].nil? ? "by_#{attribute}" : options[:view] view_name = options[:view].nil? ? "by_#{attribute}" : options[:view]
model = document.model_proxy || @model
model = (respond_to?(:model_proxy) && model_proxy ? model_proxy : @model)
# Determine the base of the search # Determine the base of the search
base = options[:proxy].nil? ? model : document.instance_eval(options[:proxy]) base = options[:proxy].nil? ? model : document.instance_eval(options[:proxy])
@ -30,7 +31,7 @@ module CouchRest
unless document.new? unless document.new?
return if docs.find{|doc| doc['id'] == document.id} return if docs.find{|doc| doc['id'] == document.id}
end end
if docs.length > 0 if docs.length > 0
document.errors.add(attribute, :taken, options.merge(:value => value)) document.errors.add(attribute, :taken, options.merge(:value => value))
end end

View file

@ -64,22 +64,6 @@ describe "Assocations" do
@invoice.alternate_client.id.should eql(@client.id) @invoice.alternate_client.id.should eql(@client.id)
end end
it "should allow to reference association by id in casted model" do
class Money < Hash
include CouchRest::Model::CastedModel
property :value, Integer
belongs_to :client
end
money = Money.new :value => 1000
money.client_id = @client.id
money.client.should eql(@client)
money.valid?.should be_true
end
end end
describe "of type collection_of" do describe "of type collection_of" do