lib/sequel/plugins/association_proxies.rb in sequel-3.47.0 vs lib/sequel/plugins/association_proxies.rb in sequel-3.48.0

- old
+ new

@@ -4,45 +4,101 @@ # method for *_to_many associations returns an array, and the association_dataset # method returns a dataset. This plugin makes the association method return a proxy # that will load the association and call a method on the association array if sent # an array method, and otherwise send the method to the association's dataset. # + # You can override which methods to forward to the dataset by passing a block to the plugin: + # + # plugin :association_proxies do |opts| + # [:find, :where, :create].include?(opts[:method]) + # end + # + # If the block returns false or nil, the method is sent to the array of associated + # objects. Otherwise, the method is sent to the association dataset. Here are the entries + # in the hash passed to the block: + # + # :method :: The name of the method + # :arguments :: The arguments to the method + # :block :: The block given to the method + # :instance :: The model instance related to the call + # :reflection :: The reflection for the association related to the call + # :proxy_argument :: The argument given to the association method call + # :proxy_block :: The block given to the association method call + # + # For example, in a call like: + # + # artist.albums(1){|ds| ds}.foo(2){|x| 3} + # + # The opts passed to the block would be: + # + # { + # :method => :foo, + # :arguments => [2], + # :block => {|x| 3}, + # :instance => artist, + # :reflection => {:name=>:albums, ...}, + # :proxy_argument => 1, + # :proxy_block => {|ds| ds} + # } + # # Usage: # # # Use association proxies in all model subclasses (called before loading subclasses) # Sequel::Model.plugin :association_proxies # # # Use association proxies in a specific model subclass # Album.plugin :association_proxies module AssociationProxies + def self.configure(model, &block) + model.instance_eval do + @association_proxy_to_dataset = block if block + @association_proxy_to_dataset ||= AssociationProxy::DEFAULT_PROXY_TO_DATASET + end + end + # A proxy for the association. Calling an array method will load the # associated objects and call the method on the associated object array. # Calling any other method will call that method on the association's dataset. class AssociationProxy < BasicObject - # Empty array used to check if an array responds to the given method. - ARRAY = [] + array = [] + # Default proc used to determine whether to sent the method to the dataset. + # If the array would respond to it, sends it to the array instead of the dataset. + DEFAULT_PROXY_TO_DATASET = proc{|opts| !array.respond_to?(opts[:method])} + # Set the association reflection to use, and whether the association should be # reloaded if an array method is called. - def initialize(instance, reflection, reload=nil) + def initialize(instance, reflection, proxy_argument, &proxy_block) @instance = instance @reflection = reflection - @reload = reload + @proxy_argument = proxy_argument + @proxy_block = proxy_block end # Call the method given on the array of associated objects if the method # is an array method, otherwise call the method on the association's dataset. def method_missing(meth, *args, &block) - (ARRAY.respond_to?(meth) ? @instance.send(:load_associated_objects, @reflection, @reload) : @instance.send(@reflection.dataset_method)). - send(meth, *args, &block) + v = if @instance.model.association_proxy_to_dataset.call(:method=>meth, :arguments=>args, :block=>block, :instance=>@instance, :reflection=>@reflection, :proxy_argument=>@proxy_argument, :proxy_block=>@proxy_block) + @instance.send(@reflection.dataset_method) + else + @instance.send(:load_associated_objects, @reflection, @proxy_argument, &@proxy_block) + end + v.send(meth, *args, &block) end end module ClassMethods + # Proc that accepts a method name, array of arguments, and block and + # should return a truthy value to send the method to the dataset instead of the + # array of associated objects. + attr_reader :association_proxy_to_dataset + + Plugins.inherited_instance_variables(self, :@association_proxy_to_dataset=>nil) + # Changes the association method to return a proxy instead of the associated objects # directly. def def_association_method(opts) - opts.returns_array? ? association_module_def(opts.association_method, opts){|*r| AssociationProxy.new(self, opts, r[0])} : super + opts.returns_array? ? association_module_def(opts.association_method, opts){|*r, &block| AssociationProxy.new(self, opts, r[0], &block)} : super end end end end end