module ActiveRecord::VirtualAttributes::VirtualDelegates::ClassMethods

Public Instance Methods

virtual_delegate(*methods) click to toggle source

Definition

# File lib/active_record/virtual_attributes/virtual_delegates.rb, line 22
def virtual_delegate(*methods)
  options = methods.extract_options!
  unless (to = options[:to])
    raise ArgumentError, 'Delegation needs an association. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter).'
  end

  to = to.to_s
  if to.include?(".") && methods.size > 1
    raise ArgumentError, 'Delegation only supports specifying a method name when defining a single virtual method'
  end

  if to.count(".") > 1
    raise ArgumentError, 'Delegation needs a single association. Supply an option hash with a :to key with only 1 period (e.g. delegate :hello, to: "greeter.greeting")'
  end

  allow_nil = options[:allow_nil]
  default = options[:default]

  # put method entry per method name.
  # This better supports reloading of the class and changing the definitions
  methods.each do |method|
    method_prefix = virtual_delegate_name_prefix(options[:prefix], to)
    method_name = "#{method_prefix}#{method}"
    if to.include?(".") # to => "target.method"
      to, method = to.split(".")
      options[:to] = to
    end

    define_delegate(method_name, method, :to => to, :allow_nil => allow_nil, :default => default)

    self.virtual_delegates_to_define =
      virtual_delegates_to_define.merge(method_name => [method, options])
  end
end

Private Instance Methods

define_delegate(method_name, method, to: nil, allow_nil: nil, default: nil) click to toggle source

see activesupport module/delegation.rb

# File lib/active_record/virtual_attributes/virtual_delegates.rb, line 82
        def define_delegate(method_name, method, to: nil, allow_nil: nil, default: nil)
          location = caller_locations(2, 1).first
          file, line = location.path, location.lineno

          # Attribute writer methods only accept one argument. Makes sure []=
          # methods still accept two arguments.
          definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block'
          default = default ? " || #{default.inspect}" : nil
          # The following generated method calls the target exactly once, storing
          # the returned value in a dummy variable.
          #
          # Reason is twofold: On one hand doing less calls is in general better.
          # On the other hand it could be that the target has side-effects,
          # whereas conceptually, from the user point of view, the delegator should
          # be doing one call.
          if allow_nil
            method_def = <<-METHOD
              def #{method_name}(#{definition})
                return self[:#{method_name}]#{default} if has_attribute?(:#{method_name})
                _ = #{to}
                if !_.nil? || nil.respond_to?(:#{method})
                  _.#{method}(#{definition})
                end#{default}
              end
            METHOD
          else
            exception = %(raise Module::DelegationError, "#{self}##{method_name} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")

            method_def = <<-METHOD
              def #{method_name}(#{definition})
                return self[:#{method_name}]#{default} if has_attribute?(:#{method_name})
                _ = #{to}
                _.#{method}(#{definition})#{default}
              rescue NoMethodError => e
                if _.nil? && e.name == :#{method}
                  #{exception}
                else
                  raise
                end
              end
            METHOD
          end
          method_def = method_def.split("\n").map(&:strip).join(';')
          module_eval(method_def, file, line)
        end
define_virtual_delegate(method_name, col, options) click to toggle source

define virtual_attribute for delegates

this is called at schema load time (and not at class definition time)

@param method_name [Symbol] name of the attribute on the source class to be defined @param col [Symbol] name of the attribute on the associated class to be referenced @option options :to [Symbol] name of the association from the source class to be referenced @option options :arel [Proc] (optional and not common) @option options :uses [Array|Symbol|Hash] sql includes hash. (default: to)

# File lib/active_record/virtual_attributes/virtual_delegates.rb, line 68
def define_virtual_delegate(method_name, col, options)
  unless (to = options[:to]) && (to_ref = reflection_with_virtual(to.to_s))
    raise ArgumentError, 'Delegation needs an association. Supply an options hash with a :to key as the last argument (e.g. delegate :hello, to: :greeter).'
  end

  col = col.to_s
  type = options[:type] || to_ref.klass.type_for_attribute(col)
  type = ActiveRecord::Type.lookup(type) if type.kind_of?(Symbol)
  raise "unknown attribute #{to}##{col} referenced in #{name}" unless type
  arel = virtual_delegate_arel(col, to_ref)
  define_virtual_attribute(method_name, type, :uses => (options[:uses] || to), :arel => arel)
end
virtual_delegate_arel(col, to_ref) click to toggle source

@param col [String] attribute name @param to_ref [Association] association from source class to target association @return [Proc] lambda to return arel that selects the attribute in a sub-query @return [Nil] if the attribute (col) can not be represented in sql.

To generate a proc, the following cases must happen:

- the column has sql (virtual_column with arel OR real sql attribute)
- the association has sql representation (a real association has sql)
- the association is to a single record (has_one or belongs_to)

See select_from_alias for examples
# File lib/active_record/virtual_attributes/virtual_delegates.rb, line 144
def virtual_delegate_arel(col, to_ref)
  # Ensure the association is reachable via sql
  #
  # But NOT ensuring the target column has sql
  #   to_ref.klass.arel_attribute(col) loads the target classes' schema.
  #   This cascades and causing a race condition
  #
  # There is currently no way to propagate sql over a virtual association
  if reflect_on_association(to_ref.name) && (to_ref.macro == :has_one || to_ref.macro == :belongs_to)
    lambda do |t|
      join_keys = if ActiveRecord.version.to_s >= "5.1"
                    to_ref.join_keys
                  else
                    to_ref.join_keys(to_ref.klass)
                  end
      src_model_id = arel_attribute(join_keys.foreign_key, t)
      blk = ->(arel) { arel.limit = 1 } if to_ref.macro == :has_one
      VirtualDelegates.select_from_alias(to_ref, col, join_keys.key, src_model_id, &blk)
    end
  end
end
virtual_delegate_name_prefix(prefix, to) click to toggle source
# File lib/active_record/virtual_attributes/virtual_delegates.rb, line 128
def virtual_delegate_name_prefix(prefix, to)
  "#{prefix == true ? to : prefix}_" if prefix
end