Sunday, May 27, 2007

Adding Annotations to JRuby Using Ruby

I love how meta-programmable Ruby is.

JRuby doesn't support annotations because Ruby doesn't support annotations. So what! We can extend Ruby to add something like annotations:


class JPABean
def self.inherited(clazz)
@@annotations = {}
end
def self.anno(annotation)
@@last_annotation = annotation
end

def self.method_added(sym)
@@annotations[sym] = @@last_annotation
end

def self.get_annotation(sym)
return @@annotations[sym]
end
end
Given this, we can do things like the following:

class MyBean :sql => "SELECT * FROM stuff WHERE name = 'foo'"
def foo; end

anno :field => :hello_id
attr :hello

anno :field => :bar_in_table
attr_accessor :bar
end
And here's some output from the resulting class:

p MyBean.get_annotation(:foo)
p MyBean.get_annotation(:hello)
p MyBean.get_annotation(:bar=)

=>

{:sql=>"SELECT * FROM stuff WHERE name = 'foo'"}
{:field=>:hello_id}
{:field=>:bar_in_table}
So as you can see we really do have all the necessary requirements to annotate classes. Now what if we just had MyBean be a java.lang.Object extension and stuffed the annotations into the resulting generated class? We can already create real Java classes in this way, but with the above syntax and a little magic in the JRuby Java integration later, we've got annotation support in on Java classes too. This should enable things like Hibernate, JPA, and JUnit 4 to work with JRuby's Ruby-based classes. Or at least, I believe it to be possible. It just requires a little work to add annotation information to the resulting generated classes.

I've planted the seed here and on the JRuby dev list...let's see if it germinates.

7 comments:

Daniel Spiewak said...

Hmm, I would prefer a syntax like this:

p MyBean.annotations[:foo]

So,

class JPABean
self.attr_accessor :annotations
end

zimbatm said...

class JPABean

def self.anno(annotation)
(@@last_annotation ||= {}).merge!(annotation)
end

def self.method_added(sym)
@@annotations[sym], @@last_annotation = @@last_annotation, {}
end

def self.get_annotation(sym)
return @@annotations[sym]
end
end

Daniel Spiewak said...

@zimba

Very nice! That should avoid overwriting the annotations. What if we expand the syntax a bit more like this:

def self.anno(*annotations)
(@@last_annotation ||= {}).merge! annotations
end

So this should let us do this:

anno :anno1 => 'My test anno1', :anno2 => 'My test anno2'

Also, we could expand this just a bit further, and alias the method:

alias :annotate, :anno

(since I rather like a full method name, rather than a cryptic abbreviation)

Antonio said...

@daniel: This bit:

anno :anno1 => 'My test anno1', :anno2 => 'My test anno2'

Would actually just make `annotations' be an array with one hash. To achieve the effect you were probably going for, you'd need to do:

anno { :anno1 => 'My test anno1' }, { :anno2 => 'My test anno2' }

Where the {}s would be optional on the second hash. Otherwise, the original definition of anno would still capture both annotations as part of the same hash.

Looks pretty rockin' :-)

Daniel Spiewak said...

@antonio

Nope, I'm going for this:

anno {:anno1 => 'My Test anno1', :anno2 => 'My Test anno2'}

(curly-braces are obviously optional) Which, come to think of it would require a slightly different method signature:

def self.anno(annotation)
(@@last_annotation ||= {}).merge!(annotation)
end

Since annotation would then be a proper, multi element hash (something I sort of failed to realize on my first pass through the comments). :-) So, zimbatm was right all along...

Anton Arhipov said...

"JRuby doesn't support annotations because Ruby doesn't support annotations."

This sounds like a jealousy in respect to groovy :)

tea42 said...

Good starter implementation. Unfortunately it gets more complex when defining annotations in modules. Facets' Annotations addressed this. Will that work in JRuby? NOTE The lib will get an official independent release soon. That'll be a good time to try it out.