Skip to content

Commit

Permalink
Merge pull request #10476 from kbrock/virtual_delegates_power
Browse files Browse the repository at this point in the history
virtual_delegate: make delegation more powerful
  • Loading branch information
Fryguy authored Sep 9, 2016
2 parents eb0a72d + 6e4a129 commit ff10a39
Show file tree
Hide file tree
Showing 2 changed files with 150 additions and 4 deletions.
70 changes: 66 additions & 4 deletions lib/extensions/ar_virtual.rb
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,34 @@ module ClassMethods
#

def virtual_delegate(*methods)
options = methods.pop
unless options.kind_of?(Hash) && options[:to]
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
delegate(*methods, options.except(:arel, :uses))

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], options[:to])
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])
Expand Down Expand Up @@ -107,6 +124,51 @@ def define_virtual_delegate(method_name, col, options)
define_virtual_attribute method_name, type, :uses => (options[:uses] || to), :arel => arel
end

# see activesupport module/delegation.rb
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 = [
"def #{method_name}(#{definition})",
"_ = #{to}",
"if !_.nil? || nil.respond_to?(:#{method})",
" _.#{method}(#{definition})",
"end#{default}",
"end"
].join ';'
else
exception = %(raise DelegationError, "#{self}##{method_name} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")

method_def = [
"def #{method_name}(#{definition})",
" _ = #{to}",
" _.#{method}(#{definition})#{default}",
"rescue NoMethodError => e",
" if _.nil? && e.name == :#{method}",
" #{exception}",
" else",
" raise",
" end",
"end"
].join ';'
end

module_eval(method_def, file, line)
end

def virtual_delegate_name_prefix(prefix, to)
"#{prefix == true ? to : prefix}_" if prefix
end
Expand Down
84 changes: 84 additions & 0 deletions spec/lib/extensions/ar_virtual_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,90 @@ def col2
end
end
end

describe ".attribute_supported_by_sql?" do
it "supports real columns" do
expect(TestClass.attribute_supported_by_sql?(:col1)).to be_truthy
end

it "supports aliases" do
TestClass.alias_attribute :col2, :col1

expect(TestClass.attribute_supported_by_sql?(:col2)).to be_truthy
end

it "does not support virtual columns" do
class TestClass
virtual_attribute :col2, :integer
def col2
col1
end
end
expect(TestClass.attribute_supported_by_sql?(:col2)).to be_falsey
end

it "supports virtual columns with arel" do
class TestClass
virtual_attribute :col2, :integer, :arel => (-> (t) { t.grouping(t.class.arel_attribute(:col1)) })
def col2
col1
end
end
expect(TestClass.attribute_supported_by_sql?(:col2)).to be_truthy
end

it "supports delegates" do
TestClass.virtual_delegate :col1, :prefix => 'parent', :to => :ref1

expect(TestClass.attribute_supported_by_sql?(:parent_col1)).to be_truthy
end
end

describe ".virtual_delegate" do
# double purposing col1. It has an actual value in the child class
let(:parent) { TestClass.create(:id => 1, :col1 => 4) }

it "delegates to child" do
TestClass.virtual_delegate :col1, :prefix => 'parent', :to => :ref1
tc = TestClass.new(:id => 2, :ref1 => parent)
expect(tc.parent_col1).to eq(4)
end

it "delegates to nil child" do
TestClass.virtual_delegate :col1, :prefix => 'parent', :to => :ref1, :allow_nil => true
tc = TestClass.new(:id => 2)
expect(tc.parent_col1).to be_nil
end

it "defines virtual attribute" do
TestClass.virtual_delegate :col1, :prefix => 'parent', :to => :ref1
expect(TestClass.virtual_attribute_names).to include("parent_col1")
end

it "defines with a new name" do
TestClass.virtual_delegate 'funky_name', :to => "ref1.col1"
tc = TestClass.new(:id => 2, :ref1 => parent)
expect(tc.funky_name).to eq(4)
end

it "defaults for to nil child (array)" do
TestClass.virtual_delegate :col1, :prefix => 'parent', :to => :ref1, :allow_nil => true, :default => []
tc = TestClass.new(:id => 2)
expect(tc.parent_col1).to eq([])
end

it "defaults for to nil child (integer)" do
TestClass.virtual_delegate :col1, :prefix => 'parent', :to => :ref1, :allow_nil => true, :default => 0
tc = TestClass.new(:id => 2)
expect(tc.parent_col1).to eq(0)
end

it "defaults for to nil child (string)" do
TestClass.virtual_delegate :col1, :prefix => 'parent', :to => :ref1, :allow_nil => true, :default => "def"
tc = TestClass.new(:id => 2)
expect(tc.parent_col1).to eq("def")
end
end
end

describe "#follow_associations" do
Expand Down

0 comments on commit ff10a39

Please sign in to comment.