Skip to content

Commit

Permalink
Implementation of default handler for custom XPath functions
Browse files Browse the repository at this point in the history
* Create tests for internal custom XPath function handler
* Allow caller to pass a nil function handler to override internal custom function handler
* Create the default custom handler as an almost totally clean slate, leaving only #method_missing and #send.
* Create documentation for custom XPath functions
  • Loading branch information
mbklein committed Dec 11, 2012
1 parent e804f5e commit 8fb3da3
Show file tree
Hide file tree
Showing 3 changed files with 244 additions and 0 deletions.
5 changes: 5 additions & 0 deletions lib/nokogiri/xml/node.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
require 'stringio'
require 'nokogiri/xml/node/save_options'
require 'nokogiri/xml/xpath_functions'

module Nokogiri
module XML
Expand Down Expand Up @@ -145,7 +146,11 @@ def search *paths
def xpath *paths
return NodeSet.new(document) unless document

default_handler = paths.include?(nil) ? nil : XPathFunctions.handler
paths, handler, ns, binds = extract_params(paths)
if handler.nil? and caller.find { |m| m =~ /:in `xpath'/ }.nil?
handler = default_handler
end

sets = paths.map { |path|
ctx = XPathContext.new(self)
Expand Down
79 changes: 79 additions & 0 deletions lib/nokogiri/xml/xpath_functions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
module Nokogiri
module XML
class XPathFunctions
@@handler = nil
class << self
def handler
@@handler ||= create_handler
end

###
# call-seq: define :sym, &block
#
# Define a new global XPath function.
#
# Nokogiri::XML::XPathFunctions.define(:regex) do |node_set, regex|
# node_set.find_all { |node| node['some_attribute'] =~ /#{regex}/ }
# end
#
# node.xpath('.//title[regex(., "\w+")]')
#
# Any custom handler passed to Node#xpath the normal way will override
# the default internal handler. Passing +nil+ will evaluate the expression
# with no custom function handler at all.
#
def define sym, &block
handler.__class__.send(:define_method, sym, &block)
end

###
# call-seq: undef :sym
#
# Undefine a previously-defined global XPath function.
#
# Nokogiri::XML::XPathFunctions.undef(:regex)
#
def undef sym
handler.__class__.send(:undef_method, sym)
end

###
# call-seq: reset!
#
# Reset the global XPath function handler to its default state
# (i.e., no user-defined functions)
#
# Nokogiri::XML::XPathFunctions.reset!
#
def reset!
@@handler = nil
end

###
# Create a handler class with NO normal methods but the bare
# necessities: #send and #method_missing.
#
# All other methods will be aliased as __#{method}__ and invoked
# via #method_missing if they haven't been overridden via #define
def create_handler
klazz = Class.new
klazz.instance_methods.each do |method|
unless method =~ /(^__)|(^send$)/
klazz.send(:alias_method,:"__#{method}__", method.to_sym)
klazz.send(:undef_method,method.to_sym)
end
end
klazz.send(:define_method, :method_missing) do |sym, *args|
if sym.to_s =~ /^__(.+)__$/
super $1.to_sym, *args
else
self.send(:"__#{sym.to_s}__", *args)
end
end
klazz.new
end
protected :create_handler
end
end
end
end
160 changes: 160 additions & 0 deletions test/xml/test_xpath_functions.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
require "helper"

module Nokogiri
module XML
class TestXPathFunctions < Nokogiri::TestCase

def setup
super

@xml = Nokogiri::XML.parse(File.read(XML_FILE), XML_FILE)

@ns = @xml.root.namespaces

# TODO: Maybe I should move this to the original code.
@ns["nokogiri"] = "http://www.nokogiri.org/default_ns/ruby/extensions_functions"

Nokogiri::XML::XPathFunctions.reset!

Nokogiri::XML::XPathFunctions.define(:things) do
@things ||= []
end

Nokogiri::XML::XPathFunctions.define(:thing) do |thing|
things << thing
thing
end

Nokogiri::XML::XPathFunctions.define(:returns_array) do |node_set|
things << node_set.to_a
node_set.to_a
end

Nokogiri::XML::XPathFunctions.define(:my_filter) do |set, attribute, value|
set.find_all { |x| x[attribute] == value }
end

Nokogiri::XML::XPathFunctions.define(:saves_node_set) do |node_set|
@things = node_set
end

@handler = Nokogiri::XML::XPathFunctions.handler
end

def test_pass_self_to_function
set = if Nokogiri.uses_libxml?
@xml.xpath('//employee/address[my_filter(., "domestic", "Yes")]')
else
@xml.xpath('//employee/address[nokogiri:my_filter(., "domestic", "Yes")]', @ns)
end
assert set.length > 0
set.each do |node|
assert_equal 'Yes', node['domestic']
end
end

def test_custom_xpath_function_gets_strings
set = @xml.xpath('//employee')
if Nokogiri.uses_libxml?
@xml.xpath('//employee[thing("asdf")]')
else
@xml.xpath('//employee[nokogiri:thing("asdf")]', @ns)
end
assert_equal(set.length, @handler.things.length)
assert_equal(['asdf'] * set.length, @handler.things)
end

def test_custom_xpath_gets_true_booleans
set = @xml.xpath('//employee')
if Nokogiri.uses_libxml?
@xml.xpath('//employee[thing(true())]')
else
@xml.xpath("//employee[nokogiri:thing(true())]", @ns)
end
assert_equal(set.length, @handler.things.length)
assert_equal([true] * set.length, @handler.things)
end

def test_custom_xpath_gets_false_booleans
set = @xml.xpath('//employee')
if Nokogiri.uses_libxml?
@xml.xpath('//employee[thing(false())]')
else
@xml.xpath("//employee[nokogiri:thing(false())]", @ns)
end
assert_equal(set.length, @handler.things.length)
assert_equal([false] * set.length, @handler.things)
end

def test_custom_xpath_gets_numbers
set = @xml.xpath('//employee')
if Nokogiri.uses_libxml?
@xml.xpath('//employee[thing(10)]')
else
@xml.xpath('//employee[nokogiri:thing(10)]', @ns)
end
assert_equal(set.length, @handler.things.length)
assert_equal([10] * set.length, @handler.things)
end

def test_custom_xpath_gets_node_sets
set = @xml.xpath('//employee/name')
if Nokogiri.uses_libxml?
@xml.xpath('//employee[thing(name)]')
else
@xml.xpath('//employee[nokogiri:thing(name)]', @ns)
end
assert_equal(set.length, @handler.things.length)
assert_equal(set.to_a, @handler.things.flatten)
end

def test_custom_xpath_gets_node_sets_and_returns_array
set = @xml.xpath('//employee/name')
if Nokogiri.uses_libxml?
@xml.xpath('//employee[returns_array(name)]')
else
@xml.xpath('//employee[nokogiri:returns_array(name)]', @ns)
end
assert_equal(set.length, @handler.things.length)
assert_equal(set.to_a, @handler.things.flatten)
end

def test_custom_xpath_handler_is_passed_a_decorated_node_set
x = Module.new do
def awesome! ; end
end
util_decorate(@xml, x)

assert @xml.xpath('//employee/name')

@xml.xpath('//employee[saves_node_set(name)]')
assert_equal @xml, @handler.things.document
assert @handler.things.respond_to?(:awesome!)
end

def test_override_custom_xpath_handler
assert_raise RuntimeError, /xmlXPathCompOpEval: function thing not found/ do
@xml.xpath('//employee[thing(name)]', nil)
end
end

def test_reset_custom_xpath_handler
Nokogiri::XML::XPathFunctions.reset!
assert_raise RuntimeError, /xmlXPathCompOpEval: function thing not found/ do
@xml.xpath('//employee[thing(name)]')
end
end

def test_replace_custom_xpath_handler
my_handler = Class.new {
def thing(node_set, initial)
node_set.find_all { |n| n.content.start_with?(initial) }
end
}.new
set = @xml.xpath('//employee[thing(name, "R")]', my_handler)
assert_equal(set.length, 2)
end

end
end
end

0 comments on commit 8fb3da3

Please sign in to comment.