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 fine
selfWhy 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