%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
# SOAP namespaces by SOAP version.
SOAPNamespace = {
1 => "http://schemas.xmlsoap.org/soap/envelope/",
2 => "http://www.w3.org/2003/05/soap-envelope"
}
# 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 (keys) and XML nodes (values) 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 to a given +block+ for wrapping the setup of multiple
# flags at once.
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 both namespaces and attributes get removed and the root node
# itself won't be included in the Hash.
#
# ==== Examples
#
# xml = '
#
#
#
#
# Gorilla
#
#
#
#
# '
#
# ApricotEatsGorilla.xml_to_hash(xml)
# # => { :body => { :authenticate_response => { :return => { :apricot => { :eats => "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? || (root.respond_to?(:size) && root.size == 0)
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.
#
# ==== Examples
#
# hash = { :apricot => { :eats => "Gorilla" } }
# ApricotEatsGorilla.hash_to_xml(hash)
#
# # => "Gorilla"
#
# hash = { :apricot => { :eats => ["Gorillas", "Snakes"] } }
# ApricotEatsGorilla.hash_to_xml(hash)
#
# # => "GorillasSnakes"
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 of a given
# +block+ into the envelope body. Accepts a Hash of additional +namespaces+
# to set. Also accepts an optional +version+ to specify the SOAP envelope
# namespace to use by SOAP version.
#
# ==== Examples
#
# ApricotEatsGorilla.soap_envelope do
# "Gorilla"
# end
#
# # => '
# # =>
# # => Gorilla
# # =>
# # => '
#
# ApricotEatsGorilla.soap_envelope(:wsdl => "http://example.com") { "pureText" }
#
# # => '
# # =>
# # => pureText
# # =>
# # => '
def soap_envelope(namespaces = {}, version = 1)
namespaces[:env] = SOAPNamespace[version] unless
namespaces[:env] || SOAPNamespace[version].nil?
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) { map_hash_value(subvalue) } if map_hash_value(subvalue)
end
end.join
end
else
xml_node(key) { map_hash_value(value) } if map_hash_value(value)
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 /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/
DateTime.parse(value)
when "true"
true
when "false"
false
else
value
end
end
# Converts Hash values into valid XML values.
def map_hash_value(value)
if value.kind_of? DateTime
value.strftime("%Y-%m-%dT%H:%M:%S")
elsif value.respond_to? :to_s
value.to_s
else
nil
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