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