require "rubygems" require "hpricot" # Apricot eats Gorilla is a SOAP communication helper. # # It translates between SOAP response messages (XML) and Ruby Hashes and may # be used to build a SOAP request envelope. It is based on CobraVsMongoose but # uses Hpricot instead of REXML and doesn't follow the BadgerFish convention. # # === xml_to_hash(xml, root_node = nil) # # xml = ' # # # # # secret # example # # # # # ' # # ApricotEatsGorilla[xml, "//return"] # # => { :auth_value => { :token => "secret", :client => "example" } } # # === hash_to_xml(hash) # # hash = { :apricot => { :eats => "Gorilla" } } # # ApricotEatsGorilla[hash] # # => "Gorilla" # # === soap_envelope(namespaces = {}) # # ApricotEatsGorilla.soap_envelope { "me" } # # # => ' # # => # # => me # # => # # => ' class ApricotEatsGorilla class << self # Class methods # Flag to enable optional sorting of Hash keys. attr_accessor :sort_keys # Flag to disable conversion of tag 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_symbol # Array of XML nodes to add a namespace to. attr_accessor :nodes_to_namespace # The namespace for nodes in :nodes_to_namespace. attr_accessor :node_namespace # Shortcut method for translating between XML Strings and Ruby Hashes. # Delegates to xml_to_hash in case +source+ is of type String or delegates # to hash_to_xml in case +source+ is of type Hash. Returns nil otherwise. # # ==== Parameters # # * +source+ - The XML String or Ruby Hash to translate. # * +root_node+ - Optional. Custom root node to start parsing the given XML. 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 object in case a +block+ was given. Nice way for setting # multiple options at once. def setup yield self if block_given? end # Converts a given +xml+ String into a Ruby Hash. Starts parsing at root # node by default. The optional +root_node+ parameter can be used to specify # a custom root node to start parsing at via XPath (Hpricot search). # The root node itself won't be included in the Hash. # # ==== Parameters # # * +xml+ - The XML String to translate into a Ruby Hash. # * +root_node+ - Optional. Custom root node to start parsing the given XML. # # ==== Examples # # xml = "Gorilla" # ApricotEatsGorilla[xml] # # => { :eats => "Gorilla" } # # xml = "Gorillas" # ApricotEatsGorilla[xml, "//eats"] # # => { :lots_of => "Gorillas" } 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 return nil if root.nil? xml_node_to_hash(root) end # Converts a given +hash+ into an XML String. # # ==== Parameters # # * +hash+ - The Ruby Hash to translate into an XML String. # # ==== Examples # # hash = { :apricot => { :eats => "Gorilla" } } # ApricotEatsGorilla[hash] # # => "Gorilla" # # hash = { :apricot => { :eats => [ "Gorillas", "Snakes" ] } } # ApricotEatsGorilla[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 from a given # +block+ into the envelope body. Accepts a Hash of +namespaces+ and their # corresponding URI for specifying custom namespaces. # # ==== Parameters # # * +namespaces+ - A Hash of namespaces and their corresponding URI. # # ==== Examples # # ApricotEatsGorilla.soap_envelope { "me" } # # # => ' # # => # # => me # # => # # => ' # # ApricotEatsGorilla.soap_envelope :wsdl => "http://example.com" do # "me" # end # # # => ' xmlns:wsdl="http://example.com" # # => xmlns:env="http://schemas.xmlsoap.org/soap/envelope/"> # # => # # => me # # => # # => ' 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 # Converts a given +string+ from CamelCase/lowerCamelCase to snake_case. def to_snake_case(string) string = string.gsub(/[A-Z]+/, '\1_\0').downcase string = string[1, string.length-1] if string[0, 1] == "_" string end # Converts a given +string+ from snake_case to lowerCamelCase. def to_lower_camel_case(string) string.to_s.gsub(/_(.)/) { $1.upcase } end private # Actual implementation for xml_to_hash. Takes and iterates through a given # Hpricot +element+ and returns a Ruby Hash equal to the given content. # # ==== Parameters # # * +element+ - The Hpricot element to translate into a Ruby Hash. def xml_node_to_hash(element) this_node = {} element.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.to_html) else key, value = child.name, xml_node_to_hash(child) end key = remove_namespace(key) key = to_snake_case(key) unless disable_hash_keys_to_snake_case key = key.intern unless disable_hash_keys_to_symbol 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 # Actual implementation for hash_to_xml. Takes a Hash key +name+ and a # value +item+ and returns an XML String equal to the given content. # # ==== Parameters # # * +name+ - A Hash key to translate into an XML String. # * +item+ - A Hash value to translate into an XML String. def nested_data_to_xml(name, item) case 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 Array value.map { |subitem| nested_data_to_xml(tag, subitem) }.join when Hash nested_data_to_xml(tag, value) else tag(tag) { value.to_s } if value.respond_to? :to_s end }.join end else tag(name) { item.to_s } if item.respond_to? :to_s end end # Creates an XML tag. Expects a block for tag content. Defaults to an empty # element tag in case no block was supplied. # # ==== Parameters # # * +name+ - The name of the XML tag. # * +attributes+ - Optional. Hash of attributes for the XML tag. def tag(name, attributes = {}) name = to_lower_camel_case(name) unless disable_tag_names_to_lower_camel_case if nodes_to_namespace.kind_of? Array name = "#{node_namespace}:#{name}" if node_namespace && nodes_to_namespace.include?(name) end 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 # Removes line breaks and whitespace between tags from a given +xml+ String. def clean_xml(xml) xml.gsub!(/\n+/, "") xml.gsub!(/(>)\s*(<)/, '\1\2') xml end # Removes the namespace from a given XML +tag+. def remove_namespace(tag) tag.sub!(/.+:(.+)/, '\1') tag end # Checks to see if a given +string+ matches "true" or "false" and converts # these values to actual Boolean objects. Returns the original string in # case it does not match "true" or "false". def booleanize(string) return true if string == "true" return false if string == "false" string end # Returns a sorted version of a given +hash+ if :sort_keys was enabled. def opt_order(hash) return hash unless sort_keys hash.keys.sort_by { |s| s.to_s }.map { |key| [key, hash[key]] } end end end