Tenderlove Making

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: ~~~ ruby doc = Nokogiri::Slop(«-eohtml)


bold hello

</html> eohtml ~~~ You may look through the tree like so: ~~~ ruby 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: ~~~ ruby 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: ~~~ ruby 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!
« go back