Question

The code here is a simplification of the bigger solution. I'm trying to figure out how to make a ruby DSL that "reads" nicely. The first block of code; works (works for now) I would like to know how to write the DSL

The core of the problem is that while the class is being processed, it doesn't have an instance variables for me to work with. IE: @match_code

Does anyone know of a simpler more elegant solution? The entirety of code must kept with in a class.

Want I want it to look like is:

class MatchStuff 
  include ProcessBase

  match 'account' do | event |
    pp event
  end
end

matcher = MatchStuff.new
matcher.accept 'account'

Current working (not so nice) code

class ProcessBase
  def initialize
    @match_code = []
  end

  def match(string_match, &block)
    @match_code.push([string_match, block])
  end

  def accept(test_str)
    @match_code.each do | test_block |
      if test_str == test_block[0])
        test_block[1].call test
      end
    end
  end
end

class MatchStuff < ProcessBase
  def initialize 
    super

    match 'account' do | event |
      pp event
    end
  end
end

test = MatchStuff.new
test.accept 'account'
Was it helpful?

Solution

In order to use methods from ProcessBase in the body of class MatchStuff, you can extend it in addition to includeing or subclassing it:

class MatchStuff
  include ProcessBase
  extend ProcessBase

  match 'account' do |event|
    pp event
  end
end

But then you have the problem that @match_code refers to a class instance variable in match, but a regular instance variable in accept. I'd add a reader method to ProcessBase and use it on the class in accept, so you always use the class instance variable. Then you can use ||= to avoid needing the initialize method (which won't get called for the class when you extend).

module ProcessBase
  # reader method, autovivifying to empty Array
  def match_code
    @match_code ||= []
  end

  def match(string_match, &block)
    # using the reader here, on self, which is the class
    match_code.push([string_match, block])
  end

  def accept(test_str)
    # here, self is the instance, so we have to explicitly get the class
    # and access match_code from there
    self.class.match_code.each do | test_block |
      if test_str == test_block[0])
        test_block[1].call test
      end
    end
  end
end

Alternatively, you could put match (with the match_code reader) and accept in separate modules, extend one and include the other. That has the advantage of not giving your instances a match method that doesn't work (because it uses the wrong match_code). You could even define an included or extended method that does one when you do the other.

module ProcessClassBase
  def match_code
    # as above
  end

  def match
    # as above
  end
end

module ProcessInstanceBase
  def accept
    # as above
  end

  def included(other_mod)
    other_mod.extend(ProcessClassBase)
  end
end

class MatchStuff
  include ProcessInstanceBase # now this also extends ProcessClassBase

  match 'account' do |event|
    pp event
  end
end

It's probably worth noting that this all breaks if you subclass MatchStuff. Instances of the subclass (call it MatchStuffSub), when accept is called, will try to access MatchStuffSub.match_code instead of MatchStuff.match_code (where match puts stuff).

OTHER TIPS

Define MatchStuff as

class MatchStuff < ProcessBase
   def set_match
      match 'account' do |event|
        pp event
      end
   end
end

and call set_match from the ProcessBase constructor:

class ProcessBase
  def initialize
    @match_code = []
    set_match # Must be defined in derived class
  end
  ...
end

Since you're not specifying a constructor for MatchStuff it will automatically call the parent class constructor.

This achieves what I interpret your goal to be, which is to specify the 'account' string and associated block in MatchStuff in as minimal a way as possible.

You could just try not using instance variables. It's not clear on what the possible values of the match strings might look like so I gave an example of how you might want to make them "safe" for this method.

module ProcessBase
  def self.included(c)
    c.extend(ClassMethods)
  end

  module ClassMethods
    def match(string_match, &block)
      method = string_match.downcase.gsub(/\s+/,'_').to_sym
      define_method(method, &block)
    end
  end

  def accept(string_match)
    method = string_match.downcase.gsub(/\s+/,'_').to_sym
    self.send(method, 'test')
  end
end

class MatchStuff
  include ProcessBase

  match 'account' do | event |
    pp event
  end
end

test = MatchStuff.new
test.accept 'account'
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top