Reputation: 6842
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
Upvotes: 4
Views: 103
Reputation: 110685
You can use the method Forwardable#def_delegator for this.
We begin by creating the classes Outer
and Settings
.
class Outer
attr_reader :settings
def initialize(&block)
instance_eval(&block)
end
def build_settings(&block)
@settings = Settings.new(&block)
end
end
class Settings
attr_accessor :a
attr_accessor :b
attr_accessor :c
def initialize(&block)
instance_eval(&block)
end
end
I have included an instance variable @settings
in Outer
to hold an instance of Settings
that will be created dynamically by Outer#build_settings
.
We now create an instance of Outer
with a block.
require 'forwardable'
outer = Outer.new do
def useful_method
'** result of the useful_method **'
end
Settings.extend Forwardable
Settings.public_send(:attr_accessor, :outer)
Settings.public_send(:def_delegator, :@outer, :useful_method)
x = self
settings = build_settings do
self.outer = x
self.a = 'aaaa'
self.b = useful_method
end
end
#=> #<Outer:0x00007ffa6f9da320 @settings=#<Settings:0x00007ffa6f9d8318
# @a="aaaa", @outer=#<Outer:0x00007ffa6f9da320 ...>,
# @b="** result of the useful_method **">>
As you see, the following operations are performed by the block.
Outer#useful_method
is createdextend
is used to include Forwardable
in Settings
' singleton class@outer
are created in Settings
useful_method
in Settings
are delegated to the value of @outer
, causing Outer#useful_method
to be invokedSettings
is created and its instance variables, @outer
, @a
and @b
, are initialized, with @outer
being set equal to the instance of Outer
just created.We can now retrieve the instance of Settings
just created and examine the values of its instance variables.
settings = outer.settings
#=> #<Settings:0x00007ffa6f9d8318 @a="aaaa",
# @outer=#<Outer:0x00007ffa6f9da320
# @settings=#<Settings:0x00007ffa6f9d8318 ...>>,
# @b="** result of the useful_method **">
settings.outer
#=> #<Outer:0x00007ffa6f9da320 @settings=#<Settings:0x00007ffa6f9d8318
# @a="aaaa", @outer=#<Outer:0x00007ffa6f9da320 ...>,
# @b="** result of the useful_method **">>
settings.a
#=> "aaaa"
settings.b
#=> "** result of the useful_method **"
Upvotes: 0
Reputation: 114178
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.
Upvotes: 3