require "rubygems"
require "hpricot"
require "iconv"
# == Apricot eats Gorilla
#
# Apricot eats Gorilla is a SOAP communication helper. It translates between
# SOAP messages (XML) and Ruby Hashes and comes with some additional helpers
# for working with SOAP services.
class ApricotEatsGorilla
class << self
# Flag to enable sorting of Hash keys.
attr_accessor :sort_keys
# Flag to disable conversion of XML tags names to lowerCamelCase.
attr_accessor :disable_tag_names_to_lower_camel_case
# Flag to disable conversion of Hash keys to snake_case.
attr_accessor :disable_hash_keys_to_snake_case
# Flag to disable conversion of Hash keys to Symbols.
attr_accessor :disable_hash_keys_to_symbols
# Hash of namespaces and XML nodes to apply these namespaces to.
attr_accessor :nodes_to_namespace
# Shortcut method for translating between XML Strings and Ruby Hashes.
# Delegates to +xml_to_hash+ in case +source+ is a String or delegates
# to +hash_to_xml+ in case +source+ is a Hash. Returns nil otherwise.
def [](source, root_node = nil)
case source
when String
xml_to_hash(source, root_node)
when Hash
hash_to_xml(source)
else
nil
end
end
# Yields this class in case a +block+ was given.
def setup
yield self if block_given?
end
# Translates a given +xml+ String into a Ruby Hash.
#
# Starts parsing at the XML root node by default. Accepts an optional
# +root_node+ parameter for defining a custom root node to start parsing
# at using an XPath (Hpricot search).
#
# The root node itself won't be included in the Hash.
#
# ==== Examples
#
# xml = '
#
#
#
#
# Gorilla
#
#
#
#
# '
#
# ApricotEatsGorilla.xml_to_hash(xml, "//return")
# # => { :apricot => { :eats => "Gorilla" } }
def xml_to_hash(xml, root_node = nil)
doc = Hpricot.XML remove_whitespace(xml)
root = root_node ? doc.search(root_node) : doc.root
return nil if root.nil?
return xml_node_to_hash(root) unless root.respond_to? :each
if root.size == 1
if root.first.children.first.kind_of?(Hpricot::Text)
map_xml_value(root.first.children.to_s)
else
xml_node_to_hash(root.first)
end
else
root.map do |node|
if node.children.first.kind_of?(Hpricot::Text)
map_xml_value(node.children.to_s)
else
xml_node_to_hash(node)
end
end
end
end
# Translates a given Ruby +hash+ into an XML String.
#
# ==== Examples
#
# hash = { :apricot => { :eats => "Gorilla" } }
#
# ApricotEatsGorilla.hash_to_xml(hash)
# # => "Gorilla"
def hash_to_xml(hash)
nested_data_to_xml(hash.keys.first, hash.values.first)
end
# Builds a SOAP request envelope and includes the content from a given
# +block+ into the envelope body. Accepts a Hash of additional +namespaces+
# to set.
#
# ==== Examples
#
# ApricotEatsGorilla.soap_envelope do
# "Gorilla"
# end
#
# # => '
# # =>
# # => Gorilla
# # =>
# # => '
def soap_envelope(namespaces = {})
namespaces[:env] = "http://schemas.xmlsoap.org/soap/envelope/"
xml_node("env:Envelope", namespaces) do
xml_node("env:Body") { yield if block_given? }
end
end
private
# Iterates through an expected Hpricot +element+ and returns a Ruby Hash
# equal to the XML content of the given element.
def xml_node_to_hash(element)
hash = {}
element.each_child do |child|
key = XMLNode.new(child.name)
key.strip_namespace!
key.to_snake_case! unless disable_hash_keys_to_snake_case
key = disable_hash_keys_to_symbols ? key.to_s : key.to_sym
# hpricot 0.6.1 returns an empty array, while 0.8 returns nil
if child.children.nil? || child.children.empty?
value = nil
elsif child.children.size == 1 && child.children.first.text?
value = map_xml_value(child.children.first.to_html)
else
value = xml_node_to_hash(child)
end
case hash[key]
when Array
hash[key] << value
when nil
hash[key] = value
else
hash[key] = [hash[key].dup, value]
end
end
hash
end
# Expects a Hash +key+ and a Hash +value+. Iterates through the given Hash
# +value+ and returns an XML String of the given Hash structure.
def nested_data_to_xml(key, value)
case value
when Array
value.map { |subitem| nested_data_to_xml(key, subitem) }.join
when Hash
xml_node(key) do
sort_hash_keys(value).map do |subkey, subvalue|
case subvalue
when Array
subvalue.map { |subitem| nested_data_to_xml(subkey, subitem) }.join
when Hash
nested_data_to_xml(subkey, subvalue)
else
xml_node(subkey) { subvalue.to_s } if subvalue.respond_to?(:to_s)
end
end.join
end
else
xml_node(key) { value.to_s } if value.respond_to?(:to_s)
end
end
# Returns an XML tag with a given +name+. Accepts a +block+ for tag content.
# Defaults to returning an empty element tag in case no block was given.
# Also accepts a Hash of +attributes+ to be added to the XML tag.
def xml_node(name, attributes = {})
node = XMLNode.new(name.to_s)
node.to_lower_camel_case! unless disable_tag_names_to_lower_camel_case
node.namespace_from_hash!(nodes_to_namespace)
node.attributes = sort_hash_keys(attributes)
node.body = yield if block_given?
node.to_tag
end
# Removes whitespace between tags from a given +xml+ String.
def remove_whitespace(xml)
xml.gsub(/(>)\s*(<)/, '\1\2')
end
# Maps an XML value to a more natural Ruby object. Converts String values
# of "true" and "false" to TrueClass and FalseClass. Converts other String
# values from "iso-8859-1" to "utf-8".
def map_xml_value(value)
return true if value == "true"
return false if value == "false"
Iconv.new("iso-8859-1", "utf-8").iconv(value)
end
# Returns a sorted version of a given +hash+ if +sort_keys+ is enabled.
def sort_hash_keys(hash)
return hash unless sort_keys
hash.keys.sort_by { |key| key.to_s }.map { |key| [ key, hash[key] ] }
end
end
end