Angular-style Dependency Injection... in Ruby?
When I first saw Angular’s automatic dependency injection, my mind was blown. It was reminiscent of the awe that I had when I first saw the magic methods ActiveRecord defines on your models in a Rails application. As I’ve been spending plenty of quality time with Angular lately, I thought it would be fun to try to bring Angular’s automatic dependency injection over to Ruby.
There is certainly no shortage of blog posts explaining how Angular’s dependency injector works, but in order to build one in Ruby, it’s vital that we understand how it works. If you’re already well versed in how Angular’s injector works, feel free to scroll ahead back to Ruby land.
Whenever you define a function in Javascript, you can ask for that function as a string:
var func = function(foo, bar) {
console.log(foo)
console.log(bar)
}
func.toString() // "function(foo, bar) { ... }"
…which initially seems like a rather useless feature. Fear not though, we have regular expressions! Let’s see if we can crack out those function arguments to do something interesting with them.
var argString = func.toString().match(/function\s\w*\(((?:\w+,?\s?)+)/)[1] // "foo, bar"
var args = argString.split(/,\s/) // ["foo", "bar"]
Cool! Those regexes are fairly opaque, but the basic idea is to match the full string of arguments, sans enclosing parens with a capture group. The inner group is noncapturing so I can treat the individual argument, comma, space sequences as a single character. Once we have that, we split on comma, space.
Using these strings, we can look up preregistered stuff in something like a hash, and then func.apply()
with the proper arguments.
objCache = {
"foo": "This can be anything you like",
"bar": "even other objects or functions"
}
var argPrep = []
for(var i = 0; i < args.length; i++) {
argPrep.push(objCache[args[i]]);
}
func.apply(this, argPrep) //=> This can be anything you like
//=> even other objects or functions
Great! Now how do we bring this over to Ruby?
Ruby-land
While reading the fantastic “Ruby Under a Microscope” by Pat Shaunessy, he demonstrated an amazing feature of MRI that allows you to view the YARV generated for anything you like.
def foo(bar)
puts bar
end
puts RubyVM::InstructionSequence.disasm(method(:foo))
== disasm: <RubyVM::InstructionSequence:foo@(irb)>======================
local table (size: 2, argc: 1 [opts: 0, rest: -1, post: 0, block: -1] s1)
[ 2] bar<Arg>
0000 trace 8 ( 1)
0002 trace 1 ( 2)
0004 putself
0005 getlocal_OP__WC__0 2
0007 opt_send_simple <callinfo!mid:puts, argc:1, FCALL|ARGS_SKIP>
0009 trace 16 ( 3)
0011 leave ( 2)
You’ll notice that one section of the output is the “local table”. In short, this is where any local variables and arguments to our function are placed. They were even kind enough to give us the name of the arugment
def parse_me(first_injected_arg, second_injected_arg)
puts first_injected_arg
puts second_injected_arg
end
RubyVM::InstructionSequence.disasm(method(:parse_me)).scan(/\[\s\d\]\s(\w+?)</).flatten
#=> ["first_injected_arg", "second_injected_arg"]
Conveniently, the argument names we’re interested in all begin with square brackets, so we target these with the regular expression. We would like to be able to call this method by just mentioning its name. A bit of aliasing fancy footwork can give us that.
require 'active_support/inflector' # We just need `camelize` and `constantize`
# feel free to write your own versions of these
def injected(method_name)
args = RubyVM::InstructionSequence.disasm(method(method_name)).scan(/\[\s\d\]\s(\w+?)</).flatten
args.map! do |injectable|
injectable.camelize.constantize.new
end
eval <<-RUBY
alias orig_#{method_name} #{method_name}
RUBY
define_method(method_name) do
send("orig_#{method_name}".to_sym, *args)
end
method_name
end
Also, since the def
keyword in Ruby 2.1 returns a symbol, defining injected methods is as simple as:
class Greeter
def say_hello
"Hi There!"
end
end
class Doge
def such_inject
"much magic"
end
end
injected def foo(greeter, doge)
puts greeter.say_hello
puts doge.such_inject
end
foo
Outputs:
Hi there!
much magic
We also made sure to return the method name as a symbol from injected
so as to allow chaining of def prefixes :). This is perhaps the most understated but awesome feature of Ruby 2.1. You could easily use this feature to also, for example, auto-wrap methods in a Mutex.synchronize with synchronized def foo
, and all other manner of method annotation goodness.
While I wouldn’t consider any of this production-ready by any stretch of the imagination, it does illustrate some nifty things we can do by prefixing def
s with other methods and regexing the generated YARV of other methods. Go forth and experiment!