22

How to simulate Java-like annotations in ruby?

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

cibercitizen1
  • 20,944
  • 16
  • 72
  • 95
  • Before that downvoting you could go to the cited url and check that my response is not a copy of it, but a simplification, that could be useful to others. (Thanks) – cibercitizen1 Jul 01 '10 at 12:07

3 Answers3

34

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
  • 363,080
  • 75
  • 446
  • 653
  • 1
    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 annotation why @__last_annotation__[meth[1..-1].to_sym] = args.first instead of @__last_annotation__[meth.to_sym] = args 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 Jul 01 '10 at 22:05
  • 1
    @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 Jul 01 '10 at 22:43
  • @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 Jul 01 '10 at 22:44
  • I had to do the following changes, for it to go with ruby 1.8 (how can I format this?)
     def method_missing(meth, *args)
        return super unless /\A_/ =~ meth.to_s
        @__last_annotation__ ||= {}
        @__last_annotation__[meth] = (args.size == 1 ? args.first : args)
      end
    
    and
    _hello    :color => 'red',   :ancho =>   23 
      _goodbye  :color => 'green', :alto =>  -123  
      _foobar   :color => 'blew' 
      def m1; end
    
    – cibercitizen1 Jul 02 '10 at 08:30
  • @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 Jul 03 '10 at 00:05
  • 3
    @jwill: It took Sun 8 years to add annotations to Java, and they can *only* be added by Sun, not by anyone else. It took me about 10 minutes and 23 lines of code to add annotations to Ruby, and anybody can do it, you don't have to rely on the language designer. I think that's pretty cool. If you want to convince me, try implementing annotations in Java in less than 30 lines. – Jörg W Mittag May 28 '15 at 00:31
4

This is the intended usage:

First you annotate a class.

class A

  extend Annotations

  extend MyAnnotations

  create_annotation("_foobar")

  _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

Then you would like do inspect A's annoations like this:

anots = A.annotations
puts anots.keys

puts anots[:m1][:_hello][:color]
puts anots[:m3][:_foobar][:color]

puts anots[:m1].key?(:_goodbye)

puts "---------------"

anots.each do |met| # each annotated method
  puts "-- annotated method --"
  puts met[0] # method name
  met[1].each do |a| # each annotation for the method
    puts "-> " + a[0].to_s # annotation name
    a[1].each do |par| # each pair: key-value
      puts " key=" +   par[0].to_s + " value=" + par[1].to_s
    end
  end
end

Well. To do that, you will need this module

module Annotations

  @@annotation_list = {}
  @@pending = {}

  def method_added(met_sym)
    #puts "-> adding " + met_sym.to_s + " to class + self.to_s
    if @@pending.size > 0
      #puts met_sym.to_s + " is annotated "
      @@annotation_list[met_sym] = @@pending
      #puts @@annotation_list
    else
      #puts met_sym.to_s + " is not annotated "
    end
    @@pending = {}
  end

  def annotate_method(a,b)
    @@pending[a] = b
  end

  def create_annotation(anot_sym)
    code = "def  #{anot_sym.to_s}(val)
      annotate_method( :#{anot_sym} ,val)
      end"
    instance_eval code
  end

  def annotations
    return @@annotation_list
  end

end

and you can define a set of annotations in a module of yours:

module MyAnnotations

  def _goodbye(val)
    annotate_method(:_goodbye, val)
  end

  def _hello(val)
    annotate_method(:_hello, val)
  end
end

or define them right into the class you are annotating:

create_annotation("_foobar")
Dathan
  • 7,266
  • 3
  • 27
  • 46
cibercitizen1
  • 20,944
  • 16
  • 72
  • 95
2

My requirement is

On a page I display a list of all instance_methods for a Class ABC, and there should be a 1 line description too with every method

Now I don' know if it is just me or storing the descriptions for all those methods with their names in a new table in the DB sounds "Super LAME"

Answer is - "Annotations"

Here's how I did it -

  1. The module Annotations given by cibercitizen1
  2. Code to include the module and activate the functionality in the desired class

Class abc.rb

class Abc
   extend Annotations
   create_annotation("_annotation")

 _annotation({:description=>"Info e-mail address"})
 def info_email
    APP_CONFIG['info_email']
 end

 _annotation({:description=>"Location of order"})
 def location
    unless self.order.blank?
      @location ||= self.order.location.description
    end
 end
  1. Code to display the given descriptions (only, not the method name) through the annotation attribute hash for the collection of instance_methods, in the view which has access to the class Abc.

VIEW methods_list.html.erb

 <html>
 <head>
 </head>
 <body> 
 <% default_description = "Description not specified" %>
  <% Abc.instance_methods.each do |method| %>
     <span style="float:right">
     <%= (Abc.annotations[method.to_sym].present?
     ?
     (Abc.annotations[method.to_sym][:_annotation][:description].blank?
     ? default_description :
     Abc.annotations[method.to_sym][:_annotation][:description])
     : default_description) %>
     </span>

   <% end %> 
 </body>  
</html>

Hope it helps!

nitinr708
  • 1,393
  • 2
  • 19
  • 29