2008-12-04 @ 10:17
Nokogiri's Slop Feature
Oops! When I released nokogiri version 1.0.7, I totally forgot to talk about Nokogiri::Slop() feature that was added. Why is it called “slop”? It lets you sloppily explore documents. Basically, it decorates your document with method_missing() that allows you to search your document via method calls.
Given this document:
doc = Nokogiri::Slop(<<-eohtml)
<html>
<body>
<p>hello</p>
<p class="bold">bold hello</p>
<body>
</html>
eohtml
You may look through the tree like so:
doc.html.body.p('.bold').text # => 'bold hello'
The way this works is that method missing is implemented on every node in the document tree. That method missing method creates an xpath or css query by using the method name and method arguments. This means that a new search is executed for every method call. It’s fun for playing around, but you definitely won’t get the same performance as using one specific CSS search.
My favorite part is that method missing is actually in the slop decorator. When you use the Nokogiri::Slop() method, it adds the decorator to a list that gets mixed in to every node instance at runtime using Module#extend. That lets me have sweet method missing action, without actually putting method missing in my Node class.
Here is a simplified example:
module Decorator
def method_a
"method a"
end
def method_b
"method b: #{super}"
end
end
class Foo
def method_b
"inside foo"
end
end
foo = Foo.new
foo.extend(Decorator)
puts foo.method_a # => 'method a'
puts foo.method_b # => 'method b: inside foo'
foo2 = Foo.new
puts foo2.method_b # => 'inside foo'
puts foo2.method_a # => NoMethodError
Module#extend is used to add functionality to the instance ‘foo’, but not ‘foo2’. Both ‘foo’ and ‘foo2’ are instances of Foo, but using Module#extend, we can conditionally add functionality without monkey patching and keeping a clean separation of concerns. You can even reach previous functionality by calling super.
But wait! There’s more! You can stack up these decorators as much as you want. For example:
module AddAString
def method
"Added a string: #{super}"
end
end
module UpperCaseResults
def method
super.upcase
end
end
class Foo
def method
"foo"
end
end
foo = Foo.new
foo.extend(AddAString)
foo.extend(UpperCaseResults)
puts foo.method # => 'ADDED A STRING: FOO'
Conditional functionality added to methods with no weird “alias method chain” involvement. Awesome!
I love ruby!