views:

30

answers:

2

I'm trying to write a class that supports nested around filters without introducing an Aspect-oriented library.

class Foo
  attr_accessor :around_filter

  def initialize
    #filters which wrap the following one are the ones with interesting logic
    #vanilla do-nothing filter
    @around_filter = lambda { yield } # or lambda {|&blk| blk.call}
  end

  def bar
    #would execute the around filters however deeply nested, then "meaty code"
    @around_filter.call do
      #meaty code here
      puts 'done' 
    end
  end

  #I expected to point only to the topmost filter, hoping to recurse
  def add_around_filter(&blk)
    prior_around_filter = @around_filter
    @around_filter = #??mystery code here?? refers to prior_around_filter
  end
end

The goal is to be able to add any number of around filters:

foo = Foo.new
foo.add_around_filter do
  puts 'strawberry'
  yield
  puts 'blueberry'
end
foo.add_around_filter do
  puts 'orange'
  yield
  puts 'grape'
end
foo.bar #=> orange, strawberry, done, blueberry, grape

I know there are a bunch of holes in the example. I wrote only enough to covey the general direction, distilling from a much larger actual class.

Although I prefer yield syntax, I'm not opposed to block references:

foo.add_around_filter do |&blk|
  puts 'orange'
  blk.call
  puts 'grape'
end

I got this working only with a single around filter. I tried lots of things with nesting, but never cracked the puzzle. If you have a solution, I'd appreciate it a bunch!

+1  A: 

One possibility is to explicitly specify the "before" and "after" part of each filter:

Code

class Foo
  def initialize
    @before_filters = Array.new
    @after_filters = Array.new
  end

  def bar
    @before_filters.each { |f| f.call }
    puts "done"
    @after_filters.each { |f| f.call }
  end

  def add_filters(options)
    @before_filters.insert(0, options[:before])
    @after_filters << options[:after]
  end
end

Usage example

>> foo = Foo.new
>> foo.add_filters(:before => lambda { puts "<body>" },
                   :after => lambda { puts "</body>" })
>> foo.add_filters(:before => lambda { puts "<html>" },
                   :after => lambda { puts "</html>" })
>> foo.bar
<html>
<body>
done
</body>
</html>
Pär Wieslander
This is actually my current implementation, but unfortunately I have a need whereby the around_filter is a better fit. Thanks, though. :)
Mario
+1  A: 

The yield in your filters will try to yield to the block defined at the point of their definition, which is not what you want. It will work with the explicit block form (in 1.9) as follows:

class Foo
  attr_accessor :around_filter

  def initialize
    #filters which wrap the following one are the ones with interesting logic
    #vanilla do-nothing filter
    @around_filter = Proc.new{|&blk| blk.call }
  end

  def bar
    #would execute the around filters however deeply nested, then "meaty code"
    @around_filter.call do
      #meaty code here
      puts 'done' 
    end
  end

  #I expected to point only to the topmost filter, hoping to recurse
  def add_around_filter(&filter)
    prior_around_filter = @around_filter
    @around_filter = Proc.new do |&blk|
      filter.call do
        prior_around_filter.call(&blk)
      end
    end
  end
end

foo = Foo.new
foo.add_around_filter do |&blk|
  puts 'strawberry'
  blk.call
  puts 'blueberry'
end
foo.add_around_filter do |&blk|
  puts 'orange'
  blk.call
  puts 'grape'
end
foo.bar #=> orange, strawberry, done, blueberry, grape
Marc-André Lafortune
That little bit of code was quite elusive. A huge thanks, Marc-André! A big help!
Mario
It's amazing what a big impact the very subtle block semantics changes in Ruby 1.9 have.
Jörg W Mittag