Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can dynamic method in outer block be accessed in child block? - Ruby Meta Programming

Tags:

ruby

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.

Classes

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

Run the code

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

Run the code (with detailed logging)

# 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

Console Log from the running code

  • Assignment to b throws an exception
  • Assignment to c works fine

enter image description here

Sample in RSpec where you don't need to assign self

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
like image 529
David Cruwys Avatar asked Aug 31 '25 03:08

David Cruwys


1 Answers

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.

like image 75
Stefan Avatar answered Sep 02 '25 16:09

Stefan