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?