Tim Raymond

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:

1
2
3
4
5
6
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.

1
2
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
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.

1
2
3
4
5
def foo(bar)
  puts bar
end

puts RubyVM::InstructionSequence.disasm(method(:foo))

Output:

== 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…

1
2
3
4
5
6
7
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.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
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

Output:

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 defs with other methods and regexing the generated YARV of other methods. Go forth and experiment!