# -*- 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 << "" 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