# -*- coding: utf-8 -*-
require 'rubygems'
require 'hpricot'
# Apricot eats Gorilla translates between XML documents and Ruby hashes.
# It's based on CobraVsMongoose but uses Hpricot instead of REXML to parse
# XML and it also doesn't follow the BadgerFish convention.
#
# Its initial purpose was to convert SOAP response messages to Ruby hashes,
# but it quickly evolved into a more general translation tool.
class ApricotEatsGorilla
# Converts XML into a Ruby Hash. Starts parsing at root node by default.
# Call with XPath expession (Hpricot search) as second parameter to define
# a custom root node. The root node itself will not be included in the Hash.
#
# E.g.
# xml = "beerappletini"
# ApricotEatsGorilla.xml_to_hash(xml)
# # => { "hates" => "appletini", "likes" => "beer" }
def self.xml_to_hash(xml, root_node = nil)
xml = clean_xml(xml)
doc = Hpricot.XML(xml)
root = root_node ? doc.at(root_node) : doc.root
xml_node_to_hash(root)
end
# Converts a Ruby Hash to XML.
#
# E.g.
# hash = { "dude" => { "likes" => "beer", "hates" => "appletini" } }
# ApricotEatsGorilla.hash_to_xml(hash)
# # => "appletinibeer"
def self.hash_to_xml(hash)
nested_data_to_xml(hash.keys.first, hash.values.first)
end
private
# Converts XML nodes into a Ruby Hash.
def self.xml_node_to_hash(node)
this_node = {}
node.each_child do |child|
if child.children.nil?
key, value = child.name, nil
elsif child.children.size == 1 && child.children.first.text?
key, value = child.name, string_to_bool?(child.children.first.raw_string)
else
key, value = child.name, xml_node_to_hash(child)
end
current = this_node[key]
case current
when Array:
this_node[key] << value
when nil
this_node[key] = value
else
this_node[key] = [current.dup, value]
end
end
this_node
end
# Converts a Ruby Hash to XML.
def self.nested_data_to_xml(name, item)
children = {}
case item
when String
make_tag(name) { item }
when Array
item.map { |subitem| nested_data_to_xml(name, subitem) }.join
when Hash
make_tag(name) do
item.map { |tag, value|
case value
when String
make_tag(tag) { value }
when Array
value.map { |subitem| nested_data_to_xml(tag, subitem) }.join
when Hash
nested_data_to_xml(tag, value)
end
}.join
end
end
end
# Helper to create an XML tag. Expects a block for tag content.
# Defaults to an empty element tag in case no block was supplied.
def self.make_tag(name)
body = yield
if body && !body.empty?
"<#{name}>" << body << "#{name}>"
else
"<#{name} />"
end
end
# Helper to remove line breaks and whitespace between XML tags.
def self.clean_xml(xml)
xml = xml.gsub(/\n+/, "")
xml = xml.gsub(/(>)\s*(<)/, '\1\2')
end
# Helper to convert "true" and "false" strings to boolean values.
# Returns the original string in case it doesn't match "true" or "false".
def self.string_to_bool?(string)
return true if string == "true"
return false if string == "false"
string
end
end