# -*- coding: utf-8 -*- require 'rubygems' require 'hpricot' # Apricot eats Gorilla is a helper for working with XML and SOAP messages. # It's based on CobraVsMongoose but without REXML and the BadgerFish convention. # Also it offers some extras for working with SOAP messages. class ApricotEatsGorilla class << self # Flag to enable optional sorting of Hash keys. Especially useful for # comparing expected values with actual results while testing. attr_accessor :sort_keys # Converts XML into a 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. # # xml = "beerappletini" # ApricotEatsGorilla.xml_to_hash(xml) # # => { "hates" => "appletini", "likes" => "beer" } def 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 Hash to XML. # # hash = { "dude" => { "likes" => "beer", "hates" => "appletini" } } # ApricotEatsGorilla.hash_to_xml(hash) # # => "appletinibeer" def hash_to_xml(hash) nested_data_to_xml(hash.keys.first, hash.values.first) end # Builds a SOAP envelope and includes the content from an expected block # into the envelope body. Pass in a Hash of namespaces and their URI to # set custom namespaces. # # # # yield # # def soap_envelope(namespaces = {}) namespaces["env"] = "http://schemas.xmlsoap.org/soap/envelope/" tag("env:Envelope", namespaces) do tag("env:Body") do yield if block_given? end end end # Creates an XML tag. Expects a block for tag content. # Defaults to an empty element tag in case no block was supplied. def tag(name, attributes = {}) return "<#{name} />" unless block_given? attr = opt_order(attributes).map { |k, v| %Q( xmlns:#{k}="#{v}") }.to_s body = (yield && !yield.empty?) ? yield : "" "<#{name}#{attr}>" << body << "" end private # Converts XML into a Hash. def xml_node_to_hash(node) this_node = {} node.each_child do |child| # hpricot 0.6.1 returns an empty array, while 0.8 returns nil if child.children.nil? || child.children.empty? key, value = child.name, nil elsif child.children.size == 1 && child.children.first.text? key, value = child.name, booleanize(child.children.first.inner_text) else key, value = child.name, xml_node_to_hash(child) end key = remove_namespace(key) 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 Hash to XML. def nested_data_to_xml(name, item) case item when String tag(name) { item } when Array item.map { |subitem| nested_data_to_xml(name, subitem) }.join when Hash tag(name) do opt_order(item).map { |tag, value| case value when String 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 # Returns a sorted version of a given Hash in case :sort_keys is enabled. def opt_order(hash) if sort_keys hash.sort_by{ |kv| kv.first } else hash end end # Helper to remove line breaks and whitespace between XML tags. def clean_xml(xml) xml = xml.gsub(/\n+/, "") xml = xml.gsub(/(>)\s*(<)/, '\1\2') end # Helper to remove namespaces from XML tags. def remove_namespace(tag) tag.sub(/.+:(.+)/, '\1') 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 booleanize(string) return true if string == "true" return false if string == "false" string end end end # Shortcut method for translating between XML documents and Hashes. # Calls xml_to_hash in case the first parameter is of type String. # Calls hash_to_xml in case the first parameter is of type Hash. # Returns nil otherwise. def ApricotEatsGorilla(source, root_node = nil) case source when String ApricotEatsGorilla.xml_to_hash(source, root_node) when Hash ApricotEatsGorilla.hash_to_xml(source) else nil end end