%w(rubygems hpricot).each do |gem|
require gem
end
# == ApricotEatsGorilla
#
# 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 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-Expression (Hpricot search).
#
# Notice that the root node itself won't be included in the Hash.
#
# ==== Example
#
# 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?
if !root.respond_to? :each
xml_node_to_hash(root)
elsif root.size == 1
single_xml_root_node_to_hash(root)
else
multiple_xml_root_nodes_to_hash(root)
end
end
# Translates a given Ruby +hash+ into an XML String.
#
# ==== Example
#
# 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 +namespaces+ to set.
#
# ==== Example
#
# 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
# Translates a single XML root node to a Ruby Hash.
def single_xml_root_node_to_hash(root)
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
end
# Translates multiple XML root nodes to a Ruby Hash.
def multiple_xml_root_nodes_to_hash(root)
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
# Iterates through a given 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 = create_hash_key(child.name)
value = create_hash_value(child)
case hash[key]
when Array
hash[key] << value
when nil
hash[key] = value
else
hash[key] = [ hash[key].dup, value ]
end
end
hash
end
# Returns a Hash key for +xml_node_to_hash+ by a given +name+.
def create_hash_key(name)
key = XMLNode.new(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
end
# Returns a Hash value for +xml_node_to_hash+ by a given +value+.
def create_hash_value(value)
if value.children.nil? || value.children.empty?
nil
elsif value.children.size == 1 && value.children.first.text?
map_xml_value(value.children.first.to_html)
else
xml_node_to_hash(value)
end
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 of a given +xml+ String.
def remove_whitespace(xml)
xml.gsub(/(>)\s*(<)/, '\1\2')
end
# Converts XML values to more natural Ruby objects.
def map_xml_value(value)
case value
when "true" then true
when "false" then false
else value
end
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