I have a DSL that allows me to write ruby code dynamically.
The Outer
class takes a custom block of code to be processed.
There is also a well-known DSL method called settings
which can take its own block of code for configuration purposes.
I want to be able to create reusable methods in the other block and have them available from within the inner block.
While writing the sample code for this post, I stumbled upon a usage that works by assigning self
to a variable
in the outer scope and calling the method on the variable
in the child scope.
I would prefer to NOT need to assign self to a variable and I noticed that if I tried to do something similar in RSPEC, then I don't need to use variable
= self
, I can define methods in parent blocks and they are available in child blocks, see the last example.
class Settings
attr_accessor :a
attr_accessor :b
attr_accessor :c
def initialize(&block)
instance_eval(&block)
end
end
class Outer
def initialize(&block)
instance_eval(&block)
end
def build_settings(&block)
Settings.new(&block)
end
end
Outer.new do
# Create a method dynamically in the main block
def useful_method
'** result of the useful_method **'
end
x = self
settings = build_settings do
self.a = 'aaaa'
self.b = useful_method() # Throws exception
self.c = x.useful_method() # Works
end
end
# Helper to colourize the console log
class String
def error; "\033[31m#{self}\033[0m" end
def success; "\033[32m#{self}\033[0m" end
end
# Run code with detailed logging
Outer.new do
# Create a method dynamically in the main block
def useful_method
'** result of the useful_method **'
end
puts "respond?: #{respond_to?(:useful_method).to_s.success}"
x = self
settings = build_settings do
puts "respond?: #{respond_to?(:useful_method).to_s.error}"
self.a = 'aaaa'
begin
self.b = useful_method().success
rescue
self.b = 'bbbb'.error
end
begin
self.c = x.useful_method().success
rescue
self.c = 'cccc'.error
end
end
puts "a: #{settings.a}"
puts "b: #{settings.b}"
puts "c: #{settings.c}"
end
b
throws an exceptionc
works fineself
Why can I access the usefull_method in the RSpec DSL, but not in my own.
RSpec.describe 'SomeTestSuite' do
context 'create method in this outer block' do
def useful_method
'david'
end
it 'use outer method in this inner block' do
expect(useful_method).to eq('david')
end
end
end
You could pass the Outer
instance to Settings.new
:
class Outer
def initialize(&block)
instance_eval(&block)
end
def build_settings(&block)
Settings.new(self, &block)
# ^^^^
end
end
and from within Settings
use method_missing
to delegate undefined method calls to outer
:
class Settings
attr_accessor :a
attr_accessor :b
attr_accessor :c
def initialize(outer, &block)
@outer = outer
instance_eval(&block)
end
private
def method_missing(name, *args, &block)
return super unless @outer.respond_to?(name)
@outer.public_send(name, *args, &block)
end
def respond_to_missing?(name, include_all = false)
@outer.respond_to?(name, include_all) or super
end
end
This way, useful_method
can be called without an explicit receiver.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With