How do I "fake" C# style attributes in Ruby?

attributes, ruby

Solution

I have a somewhat different approach:

class Object
  def self.profile(method_name)
    return_value = nil
    time = Benchmark.measure do
      return_value = yield
    end

    puts "#{method_name} finished in #{time.real}"
    return_value
  end
end

require "benchmark"

module Profiler
  def method_added(name)
    profile_method(name) if @method_profiled
    super
  end

  def profile_method(method_name)
    @method_profiled = nil
    alias_method "unprofiled_#{method_name}", method_name
    class_eval <<-ruby_eval
      def #{method_name}(*args, &blk)
        name = "\#{self.class}##{method_name}"
        msg = "\#{name} was called with \#{args.inspect}"
        msg << " and a block" if block_given?
        puts msg

        Object.profile(name) { unprofiled_#{method_name}(*args, &blk) }
      end
    ruby_eval
  end

  def profile
    @method_profiled = true
  end
end

module Comment
  def method_added(name)
    comment_method(name) if @method_commented
    super
  end

  def comment_method(method_name)
    comment = @method_commented
    @method_commented = nil
    alias_method "uncommented_#{method_name}", method_name
    class_eval <<-ruby_eval
      def #{method_name}(*args, &blk)
        puts #{comment.inspect}
        uncommented_#{method_name}(*args, &blk)
      end
    ruby_eval
  end

  def comment(text)
    @method_commented = text
  end
end

class Foo
  extend Profiler
  extend Comment

  # This is the fake attribute, it profiles a single method.
  profile
  def bar(b)
   puts b
  end

  def barbar(b)
    puts(b)
  end

  comment("this really should be fixed")
  def snafu(b)
  end
end

A few points about this solution:

- I provided the additional methods via modules which could be extended into new classes as needed. This avoids polluting the global namespace for all modules.

- I avoided using alias_method, since module includes allow AOP-style extensions (in this case, for `method_added`) without the need for aliasing.

- I chose to use `class_eval` rather than `define_method` to define the new method in order to be able to support methods that take blocks. This also necessitated the use of `alias_method`.

- Because I chose to support blocks, I also added a bit of text to the output in case the method takes a block.

- There are ways to get the actual parameter names, which would be closer to your original output, but they don't really fit in a response here. You can check out merb-action-args, where we wrote some code that required getting the actual parameter names. It works in JRuby, Ruby 1.8.x, Ruby 1.9.1 (with a gem), and Ruby 1.9 trunk (natively).

- The basic technique here is to store a class instance variable when `profile` or `comment` is called, which is then applied when a method is added. As in the previous solution, the `method_added` hook is used to track when the new method is added, but instead of removing the hook each time, the hook checks for an instance variable. The instance variable is removed after the AOP is applied, so it only applies once. If this same technique was used multiple time, it could be further abstracted.

- In general, I tried to stick as close to your "spec" as possible, which is why I included the Object.profile snippet instead of implementing it inline.

Problem

EDIT: I slightly changed the spec, to better match what I imagined this to do. Well, I don't really want to fake C# attributes, I want to one-up-them and support AOP as well. Given the program: ``` class Object def Object.profile # magic code here end end class Foo # This is the fake attribute, it profiles a single method. profile def bar(b) puts b end def barbar(b) puts(b) end comment("this really should be fixed") def snafu(b) end end Foo.new.bar("test") Foo.new.barbar("test") puts Foo.get_comment(:snafu) ``` Desired output: ``` Foo.bar was called with param: b = "test" test Foo.bar call finished, duration was 1ms test This really should be fixed ``` Is there any way to achieve this?

Original source