views:

138

answers:

2

How to simulate Java-like annotations in ruby?

(We'll I have the answer, generalizing http://bens.me.uk/2009/java-style-annotations-in-ruby)

A: 
cibercitizen1
+3  A: 

This is adapted from a piece of code I wrote in an answer to another question a couple of weeks ago, although it is of course hardly original. This is a well-known Ruby idiom, after all, which has been in use for many years, at least since rakes's desc method.

module Annotations
  def annotations(meth=nil)
    return @__annotations__[meth] if meth
    @__annotations__
  end

  private

  def method_added(m)
    (@__annotations__ ||= {})[m] = @__last_annotation__ if @__last_annotation__
    @__last_annotation__ = nil
    super
  end

  def method_missing(meth, *args)
    return super unless /\A_/ =~ meth
    @__last_annotation__ ||= {}
    @__last_annotation__[meth[1..-1].to_sym] = args.size == 1 ? args.first : args
  end
end

class Module
  private

  def annotate!
    extend Annotations
  end
end

Here's a small example:

class A
  annotate!

  _hello   color: 'red',   ancho:   23
  _goodbye color: 'green', alto:  -123
  _foobar  color: 'blew'
  def m1; end

  def m2; end

  _foobar  color: 'cyan'
  def m3; end
end

And of course no Ruby code would be complete without a testsuite:

require 'test/unit'
class TestAnnotations < Test::Unit::TestCase
  def test_that_m1_is_annotated_with_hello_and_has_value_red
    assert_equal 'red', A.annotations(:m1)[:hello][:color]
  end
  def test_that_m3_is_annotated_with_foobar_and_has_value_cyan
    assert_equal 'cyan', A.annotations[:m3][:foobar][:color]
  end
  def test_that_m1_is_annotated_with_goodbye
    assert A.annotations[:m1][:goodbye]
  end
  def test_that_all_annotations_are_there
    annotations = {
      m1: {
        hello:   { color: 'red',   ancho:   23 },
        goodbye: { color: 'green', alto:  -123 },
        foobar:  { color: 'blew'               }
      },
      m3: {
        foobar:  { color: 'cyan'               }
      }
    }
    assert_equal annotations, A.annotations
  end
end
Jörg W Mittag
Your code is simpler to use: it doesn't require the annotations to be declared in advance! A question: When you store the arguments of the annotationwhy <code> @__last_annotation__[meth[1..-1].to_sym] = args.first</code>instead of <code>@__last_annotation__[meth.to_sym] = args</code>I can't catch bot meth[1..-1], or either args.first (isn't it only the first element of the array, that packs the arguments).
cibercitizen1
@cibercitizen1: The `meth[1..-1]` basically says "everything except the first character of the method name", IOW it removes the underscore. The `args.first` is just because I only want the method to take one argument, the hash with the annotation key-value-pairs. But I have to define `method_missing` to take an arbitrary number of arguments just so that I can send them on if I don't want to handle the method myself (i.e. if it doesn't start with an underscore). After all, there could be other definitions of `method_missing` in the system for other DSLs or for Rails or something.
Jörg W Mittag
@cibercitizen1: You could do something more clever of course, like check if the size is `1` and then unwrap it and otherwise just leave it as an array.
Jörg W Mittag
@Jörg, thank you very much !
cibercitizen1
<html>I had to do the following changes, for it to go with ruby 1.8(how can I format this?)<pre> def method_missing(meth, *args) return super unless /\A_/ =~ meth.to_s @__last_annotation__ ||= {} @__last_annotation__[meth] = (args.size == 1 ? args.first : args) end</pre>and<pre>_hello :color => 'red', :ancho => 23 _goodbye :color => 'green', :alto => -123 _foobar :color => 'blew' def m1; end</pre></html>
cibercitizen1
@cibercitizen, you don't need to use those html style tags here. They don't work anyway. You can use some of the features of markdown.
BobbyShaftoe