%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_datetime value.to_datetime.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