mirror of
https://github.com/ms-ati/docile
synced 2023-03-27 23:21:52 -04:00
Allow helper methods defined in the block's context to call DSL methods
Previously, it turns out that this wasn't possible, which made refactoring code that used Docile to extract common helper methods, and re-use them in different blocks, a painful experience. This change should make it possible to extract methods from blocks into the context around the block, and have those extracted helper methods still able to call methods on the DSL object.
This commit is contained in:
parent
b66d9b0bf3
commit
745b317e12
3 changed files with 125 additions and 7 deletions
|
@ -22,8 +22,13 @@ module Docile
|
||||||
value_from_block = block_context.instance_variable_get(ivar)
|
value_from_block = block_context.instance_variable_get(ivar)
|
||||||
proxy_context.instance_variable_set(ivar, value_from_block)
|
proxy_context.instance_variable_set(ivar, value_from_block)
|
||||||
end
|
end
|
||||||
|
|
||||||
proxy_context.instance_exec(*args, &block)
|
proxy_context.instance_exec(*args, &block)
|
||||||
ensure
|
ensure
|
||||||
|
if block_context.respond_to?(:__docile_undo_fallback__)
|
||||||
|
block_context.send(:__docile_undo_fallback__)
|
||||||
|
end
|
||||||
|
|
||||||
block_context.instance_variables.each do |ivar|
|
block_context.instance_variables.each do |ivar|
|
||||||
value_from_dsl_proxy = proxy_context.instance_variable_get(ivar)
|
value_from_dsl_proxy = proxy_context.instance_variable_get(ivar)
|
||||||
block_context.instance_variable_set(ivar, value_from_dsl_proxy)
|
block_context.instance_variable_set(ivar, value_from_dsl_proxy)
|
||||||
|
|
|
@ -38,6 +38,31 @@ module Docile
|
||||||
def initialize(receiver, fallback)
|
def initialize(receiver, fallback)
|
||||||
@__receiver__ = receiver
|
@__receiver__ = receiver
|
||||||
@__fallback__ = fallback
|
@__fallback__ = fallback
|
||||||
|
|
||||||
|
# Enables calling DSL methods from helper methods in the block's context
|
||||||
|
unless fallback.respond_to?(:method_missing)
|
||||||
|
# NOTE: There's no {#define_singleton_method} on Ruby 1.8.x
|
||||||
|
singleton_class = (class << fallback; self; end)
|
||||||
|
|
||||||
|
# instrument {#method_missing} on the block's context to fallback to
|
||||||
|
# the DSL object. This allows helper methods in the block's context to
|
||||||
|
# contain calls to methods on the DSL object.
|
||||||
|
singleton_class.
|
||||||
|
send(:define_method, :method_missing) do |method, *args, &block|
|
||||||
|
if receiver.respond_to?(method.to_sym)
|
||||||
|
receiver.__send__(method.to_sym, *args, &block)
|
||||||
|
else
|
||||||
|
super(method, *args, &block)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# instrument a helper method to remove the above instrumentation
|
||||||
|
singleton_class.
|
||||||
|
send(:define_method, :__docile_undo_fallback__) do
|
||||||
|
singleton_class.send(:remove_method, :method_missing)
|
||||||
|
singleton_class.send(:remove_method, :__docile_undo_fallback__)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
# @return [Array<Symbol>] Instance variable names, excluding
|
# @return [Array<Symbol>] Instance variable names, excluding
|
||||||
|
|
|
@ -117,6 +117,94 @@ describe Docile do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
context "when block's context has helper methods which call DSL methods" do
|
||||||
|
class BlockContextWithHelperMethods
|
||||||
|
def initialize(array_as_dsl)
|
||||||
|
@array_as_dsl = array_as_dsl
|
||||||
|
end
|
||||||
|
|
||||||
|
# Classic dynamic programming factorial, using the methods of {Array}
|
||||||
|
# as a DSL to implement it, via helper methods {#calculate_factorials}
|
||||||
|
# and {#save_factorials} which are defined in this class, so therefore
|
||||||
|
# outside the block.
|
||||||
|
def factorial_as_dsl_against_array(n)
|
||||||
|
Docile.dsl_eval(@array_as_dsl) { calculate_factorials(n) }.last
|
||||||
|
end
|
||||||
|
|
||||||
|
# Uses the helper method {#save_factorials} below.
|
||||||
|
def calculate_factorials(n)
|
||||||
|
(2..n).each { |i| save_factorial(i) }
|
||||||
|
end
|
||||||
|
|
||||||
|
# Uses the methods {Array#push} and {Array#at} as a DSL from a helper
|
||||||
|
# method defined in the block's context. Successfully calling this
|
||||||
|
# proves that we can find helper methods from outside the block, and
|
||||||
|
# then find DSL methods from inside those helper methods.
|
||||||
|
def save_factorial(i)
|
||||||
|
push(i * at(i - 1))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { context.method(:factorial_as_dsl_against_array) }
|
||||||
|
|
||||||
|
let(:context) { BlockContextWithHelperMethods.new(array_as_dsl) }
|
||||||
|
|
||||||
|
let(:array_as_dsl) { [1, 1] }
|
||||||
|
|
||||||
|
it "finds DSL methods within helper method defined in block's context" do
|
||||||
|
# see https://en.wikipedia.org/wiki/Factorial
|
||||||
|
[
|
||||||
|
[0, 1],
|
||||||
|
[1, 1],
|
||||||
|
[2, 2],
|
||||||
|
[3, 6],
|
||||||
|
[4, 24],
|
||||||
|
[5, 120],
|
||||||
|
[6, 720],
|
||||||
|
[7, 5_040],
|
||||||
|
[8, 40_320],
|
||||||
|
[9, 362_880],
|
||||||
|
[10, 3_628_800],
|
||||||
|
[11, 39_916_800],
|
||||||
|
[12, 479_001_600],
|
||||||
|
[13, 6_227_020_800],
|
||||||
|
[14, 87_178_291_200],
|
||||||
|
[15, 1_307_674_368_000]
|
||||||
|
].each do |n, expected_factorial|
|
||||||
|
array_as_dsl.replace([1, 1])
|
||||||
|
expect(subject.call(n)).to eq expected_factorial
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it "removes fallback instrumentation from the DSL object after block" do
|
||||||
|
expect { subject.call(5) }.
|
||||||
|
not_to change { context.respond_to?(:method_missing) }.
|
||||||
|
from(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "removes method to remove fallbacl from the DSL object after block" do
|
||||||
|
expect { subject.call(5) }.
|
||||||
|
not_to change { context.respond_to?(:__docile_undo_fallback__) }.
|
||||||
|
from(false)
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when helper methods call methods that are undefined" do
|
||||||
|
let(:array_as_dsl) { "not an array" }
|
||||||
|
|
||||||
|
it "raises NoMethodError" do
|
||||||
|
expect { subject.call(5) }.
|
||||||
|
to raise_error(NoMethodError, /undefined method `at' /)
|
||||||
|
end
|
||||||
|
|
||||||
|
it "removes fallback instrumentation from the DSL object after block" do
|
||||||
|
expect { subject.call(5) rescue nil }.
|
||||||
|
not_to change { context.respond_to?(:method_missing) }.
|
||||||
|
from(false)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context "when DSL have NoMethod error inside" do
|
||||||
class DSLWithNoMethod
|
class DSLWithNoMethod
|
||||||
def initialize(b); @b = b; end
|
def initialize(b); @b = b; end
|
||||||
attr_accessor :b
|
attr_accessor :b
|
||||||
|
@ -125,10 +213,10 @@ describe Docile do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
context "when DSL have NoMethod error inside" do
|
it "raise NoMethodError error from nil" do
|
||||||
it "raise error from nil" do
|
|
||||||
Docile.dsl_eval(DSLWithNoMethod.new(nil)) do
|
Docile.dsl_eval(DSLWithNoMethod.new(nil)) do
|
||||||
expect { push_element }.to raise_error(NoMethodError, /undefined method `push' (for|on) nil:NilClass/)
|
expect { push_element }.
|
||||||
|
to raise_error(NoMethodError, /undefined method `push' (for|on) nil:NilClass/)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue