require "builder"
require "savon"
require "savon/core_ext/object"
require "savon/core_ext/string"
require "savon/core_ext/symbol"
require "savon/core_ext/array"
require "savon/core_ext/datetime"
module Savon
module CoreExt
module Hash
# Returns the values from the soap:Body element or an empty Hash in case the soap:Body tag could
# not be found.
def find_soap_body
envelope = self[keys.first] || {}
body_key = envelope.keys.find { |key| /.+:Body/ =~ key } rescue nil
body_key ? envelope[body_key].map_soap_response : {}
end
# Translates the Hash into SOAP request compatible XML.
#
# { :find_user => { :id => 123, "wsdl:Key" => "api" } }.to_soap_xml
# # => "123api"
#
# ==== Mapping
#
# * Hash keys specified as Symbols are converted to lowerCamelCase Strings
# * Hash keys specified as Strings are not converted and may contain namespaces
# * DateTime values are converted to xs:dateTime Strings
# * Objects responding to to_datetime (except Strings) are converted to xs:dateTime Strings
# * TrueClass and FalseClass objects are converted to "true" and "false" Strings
# * All other objects are expected to be converted to Strings using to_s
#
# An example:
#
# { :magic_request => {
# :perform_move => true,
# "perform_at" => DateTime.new(2010, 11, 22, 11, 22, 33)
# }
# }.to_soap_xml
#
#
# true
# 2012-06-11T10:42:21
#
#
# ==== Escaped XML values
#
# By default, special characters in XML String values are escaped.
#
# ==== Fixed order of XML tags
#
# In case your service requires the tags to be in a specific order (parameterOrder), you have two
# options. The first is to specify your body as an XML string. The second is to specify the order
# through an additional array stored under the +:order!+ key.
#
# { :name => "Eve", :id => 123, :order! => [:id, :name] }.to_soap_xml
# # => "123Eve"
#
# ==== XML attributes
#
# If you need attributes, you could either go with an XML string or add another hash under the
# +:attributes!+ key.
#
# { :person => "Eve", :attributes! => { :person => { :id => 666 } } }.to_soap_xml
# # => 'Eve'
def to_soap_xml
xml = Builder::XmlMarkup.new
attributes = delete(:attributes!) || {}
order.each do |key|
attrs = attributes[key] || {}
value = self[key]
escape_xml = key.to_s[-1, 1] != "!"
key = key.to_soap_key
case value
when ::Array then xml << value.to_soap_xml(key, escape_xml, attrs)
when ::Hash then xml.tag!(key, attrs) { xml << value.to_soap_xml }
when NilClass then xml.tag!(key, "xsi:nil" => "true")
else xml.tag!(key, attrs) { xml << (escape_xml ? value.to_soap_value : value.to_soap_value!) }
end
end
xml.target!
end
# Maps keys and values of a Hash created from SOAP response XML to more convenient Ruby Objects.
def map_soap_response
inject({}) do |hash, (key, value)|
value = case value
when ::Hash then value["xsi:nil"] ? nil : value.map_soap_response
when ::Array then value.map { |val| val.map_soap_response rescue val }
when ::String then value.map_soap_response
end
new_key = if Savon.strip_namespaces?
key.strip_namespace.snakecase.to_sym
else
key.snakecase
end
if hash[new_key] # key already exists, value should be added as an Array
hash[new_key] = [hash[new_key], value].flatten
result = hash
else
result = hash.merge new_key => value
end
result
end
end
private
# Deletes and returns an Array of keys stored under the :order! key. Defaults to return the actual
# keys of this Hash if no :order! key could be found. Raises an ArgumentError in case the :order!
# Array does not match the Hash keys.
def order
order = delete :order!
order = keys unless order.kind_of? ::Array
missing, spurious = keys - order, order - keys
raise ArgumentError, "Missing elements in :order! #{missing.inspect}" unless missing.empty?
raise ArgumentError, "Spurious elements in :order! #{spurious.inspect}" unless spurious.empty?
order
end
end
end
end
Hash.send :include, Savon::CoreExt::Hash