savon 0.7.9 → 0.8.0.beta.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (74) hide show
  1. data/.gitignore +9 -0
  2. data/.rspec +1 -0
  3. data/.yardopts +2 -0
  4. data/CHANGELOG.md +332 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +20 -0
  7. data/README.md +37 -0
  8. data/Rakefile +28 -39
  9. data/autotest/discover.rb +1 -0
  10. data/lib/savon.rb +10 -31
  11. data/lib/savon/client.rb +116 -98
  12. data/lib/savon/core_ext/array.rb +36 -22
  13. data/lib/savon/core_ext/datetime.rb +15 -6
  14. data/lib/savon/core_ext/hash.rb +122 -94
  15. data/lib/savon/core_ext/object.rb +19 -11
  16. data/lib/savon/core_ext/string.rb +62 -57
  17. data/lib/savon/core_ext/symbol.rb +13 -5
  18. data/lib/savon/error.rb +6 -0
  19. data/lib/savon/global.rb +75 -0
  20. data/lib/savon/http/error.rb +42 -0
  21. data/lib/savon/soap.rb +8 -283
  22. data/lib/savon/soap/fault.rb +48 -0
  23. data/lib/savon/soap/request.rb +61 -0
  24. data/lib/savon/soap/response.rb +65 -0
  25. data/lib/savon/soap/xml.rb +132 -0
  26. data/lib/savon/version.rb +2 -2
  27. data/lib/savon/wsdl/document.rb +107 -0
  28. data/lib/savon/wsdl/parser.rb +90 -0
  29. data/lib/savon/wsdl/request.rb +35 -0
  30. data/lib/savon/wsse.rb +42 -104
  31. data/savon.gemspec +26 -0
  32. data/spec/fixtures/response/response_fixture.rb +26 -26
  33. data/spec/fixtures/response/xml/list.xml +18 -0
  34. data/spec/fixtures/wsdl/wsdl_fixture.rb +6 -0
  35. data/spec/fixtures/wsdl/wsdl_fixture.yml +4 -4
  36. data/spec/savon/client_spec.rb +274 -51
  37. data/spec/savon/core_ext/datetime_spec.rb +1 -1
  38. data/spec/savon/core_ext/hash_spec.rb +40 -4
  39. data/spec/savon/core_ext/object_spec.rb +1 -1
  40. data/spec/savon/core_ext/string_spec.rb +0 -12
  41. data/spec/savon/http/error_spec.rb +52 -0
  42. data/spec/savon/savon_spec.rb +90 -0
  43. data/spec/savon/soap/fault_spec.rb +80 -0
  44. data/spec/savon/soap/request_spec.rb +45 -0
  45. data/spec/savon/soap/response_spec.rb +153 -0
  46. data/spec/savon/soap/xml_spec.rb +249 -0
  47. data/spec/savon/soap_spec.rb +4 -177
  48. data/spec/savon/{wsdl_spec.rb → wsdl/document_spec.rb} +54 -17
  49. data/spec/savon/wsdl/request_spec.rb +15 -0
  50. data/spec/savon/wsse_spec.rb +123 -92
  51. data/spec/spec_helper.rb +19 -4
  52. data/spec/support/endpoint.rb +25 -0
  53. metadata +97 -97
  54. data/.autotest +0 -5
  55. data/CHANGELOG +0 -176
  56. data/README.rdoc +0 -64
  57. data/lib/savon/core_ext.rb +0 -8
  58. data/lib/savon/core_ext/net_http.rb +0 -19
  59. data/lib/savon/core_ext/uri.rb +0 -10
  60. data/lib/savon/logger.rb +0 -56
  61. data/lib/savon/request.rb +0 -138
  62. data/lib/savon/response.rb +0 -174
  63. data/lib/savon/wsdl.rb +0 -137
  64. data/lib/savon/wsdl_stream.rb +0 -85
  65. data/spec/basic_spec_helper.rb +0 -11
  66. data/spec/endpoint_helper.rb +0 -23
  67. data/spec/http_stubs.rb +0 -26
  68. data/spec/integration/http_basic_auth_spec.rb +0 -16
  69. data/spec/integration/server.rb +0 -51
  70. data/spec/savon/core_ext/net_http_spec.rb +0 -38
  71. data/spec/savon/core_ext/uri_spec.rb +0 -19
  72. data/spec/savon/request_spec.rb +0 -117
  73. data/spec/savon/response_spec.rb +0 -179
  74. data/spec/spec.opts +0 -4
@@ -0,0 +1 @@
1
+ Autotest.add_discovery { "rspec2" }
@@ -1,35 +1,14 @@
1
- module Savon
1
+ require "savon/version"
2
+ require "savon/global"
3
+ require "savon/client"
2
4
 
3
- # Raised in case of an HTTP error.
4
- class HTTPError < StandardError; end
5
+ module Savon
6
+ extend Global
5
7
 
6
- # Raised in case of a SOAP fault.
7
- class SOAPFault < StandardError; end
8
+ # Yields this module to a given +block+. Please refer to the
9
+ # <tt>Savon::Global</tt> module for configuration options.
10
+ def self.configure
11
+ yield self if block_given?
12
+ end
8
13
 
9
14
  end
10
-
11
- # standard libs
12
- require "logger"
13
- require "net/https"
14
- require "base64"
15
- require "digest/sha1"
16
- require "rexml/document"
17
- require "stringio"
18
- require "zlib"
19
- require "cgi"
20
-
21
- # gem dependencies
22
- require "builder"
23
- require "crack/xml"
24
-
25
- # core files
26
- require "savon/core_ext"
27
- require "savon/wsse"
28
- require "savon/soap"
29
- require "savon/logger"
30
- require "savon/request"
31
- require "savon/response"
32
- require "savon/wsdl_stream"
33
- require "savon/wsdl"
34
- require "savon/client"
35
- require "savon/version"
@@ -1,130 +1,148 @@
1
+ require "httpi/request"
2
+ require "savon/soap/xml"
3
+ require "savon/soap/request"
4
+ require "savon/soap/response"
5
+ require "savon/wsdl/document"
6
+ require "savon/wsse"
7
+
1
8
  module Savon
2
9
 
3
10
  # = Savon::Client
4
11
  #
5
12
  # Savon::Client is the main object for connecting to a SOAP service. It includes methods to access
6
- # both the Savon::WSDL and Savon::Request object.
7
- #
8
- # == Instantiation
9
- #
10
- # Depending on whether you aim to use Savon with or without Savon::WSDL, you need to instantiate
11
- # Savon::Client by passing in the WSDL or SOAP endpoint.
12
- #
13
- # Client instance with a WSDL endpoint:
14
- #
15
- # client = Savon::Client.new "http://example.com/UserService?wsdl"
16
- #
17
- # Client instance with a SOAP endpoint (for using Savon without a WSDL):
18
- #
19
- # client = Savon::Client.new "http://example.com/UserService"
20
- #
21
- # It is recommended to not use Savon::WSDL for production. Please take a look at the Documentation
22
- # for Savon::WSDL for more information about how to disable it.
23
- #
24
- # == Using a proxy server
25
- #
26
- # You can specify the URI to a proxy server via optional hash arguments.
27
- #
28
- # client = Savon::Client.new "http://example.com/UserService?wsdl", :proxy => "http://proxy.example.com"
29
- #
30
- # == Forcing a particular SOAP endpoint
31
- #
32
- # In case you don't want to use the SOAP endpoint specified in the WSDL, you can tell Savon to use
33
- # another SOAP endpoint.
34
- #
35
- # client = Savon::Client.new "http://example.com/UserService?wsdl", :soap_endpoint => "http://localhost/UserService"
36
- #
37
- # == Gzipped SOAP requests
38
- #
39
- # Sending gzipped SOAP requests can be specified per client instance.
40
- #
41
- # client = Savon::Client.new "http://example.com/UserService?wsdl", :gzip => true
42
- #
43
- # == Savon::WSDL
44
- #
45
- # You can access Savon::WSDL via:
46
- #
47
- # client.wsdl
48
- #
49
- # == Savon::Request
50
- #
51
- # You can also access Savon::Request via:
52
- #
53
- # client.request
13
+ # both the Savon::WSDL::Document and HTTPI::Request object.
54
14
  class Client
55
15
 
56
- # Expects a SOAP +endpoint+ string. Also accepts a Hash of +options+.
16
+ # Initializes the Savon::Client for a SOAP service. Accepts a +block+ which is evaluated in the
17
+ # context of this object to let you access the +wsdl+, +http+, and +wsse+ methods.
18
+ #
19
+ # == Examples
57
20
  #
58
- # ==== Options:
21
+ # # Using a remote WSDL
22
+ # client = Savon::Client.new { wsdl.document = "http://example.com/UserService?wsdl" }
59
23
  #
60
- # [proxy] the proxy server to use
61
- # [gzip] whether to gzip SOAP requests
62
- # [soap_endpoint] force to use this SOAP endpoint
63
- def initialize(endpoint, options = {})
64
- soap_endpoint = options.delete(:soap_endpoint)
65
- @request = Request.new endpoint, options
66
- @wsdl = WSDL.new @request, soap_endpoint
24
+ # # Using a local WSDL
25
+ # client = Savon::Client.new { wsdl.document = "../wsdl/user_service.xml" }
26
+ #
27
+ # # Directly accessing a SOAP endpoint
28
+ # client = Savon::Client.new do
29
+ # wsdl.endpoint = "http://example.com/UserService"
30
+ # wsdl.namespace = "http://users.example.com"
31
+ # end
32
+ def initialize(&block)
33
+ process 1, &block if block
34
+ wsdl.request = http
67
35
  end
68
36
 
69
- # Returns the Savon::WSDL.
70
- attr_reader :wsdl
37
+ # Returns the <tt>Savon::WSDL::Document</tt>.
38
+ def wsdl
39
+ @wsdl ||= WSDL::Document.new
40
+ end
71
41
 
72
- # Returns the Savon::Request.
73
- attr_reader :request
42
+ # Returns the <tt>HTTPI::Request</tt>.
43
+ def http
44
+ @http ||= HTTPI::Request.new
45
+ end
74
46
 
75
- # Returns +true+ for available methods and SOAP actions.
76
- def respond_to?(method)
77
- return true if @wsdl.respond_to? method
78
- super
47
+ # Returns the <tt>Savon::WSSE</tt> object.
48
+ def wsse
49
+ @wsse ||= WSSE.new
79
50
  end
80
51
 
81
- # Same as method_missing. Workaround for SOAP actions that method_missing does not catch
82
- # because the method does exist.
83
- def call(method, *args, &block)
84
- method_missing method, *args, &block
52
+ # Returns the <tt>Savon::SOAP::XML</tt> object. Please notice, that this object is only available
53
+ # in a block given to <tt>Savon::Client#request</tt>. A new instance of this object is created
54
+ # per SOAP request.
55
+ attr_reader :soap
56
+
57
+ # Executes a SOAP request for a given SOAP action. Accepts a +block+ which is evaluated in the
58
+ # context of this object to let you access the +soap+, +wsdl+, +http+ and +wsse+ methods.
59
+ #
60
+ # == Examples
61
+ #
62
+ # # Calls a "getUser" SOAP action with the payload of "<userId>123</userId>"
63
+ # client.request(:get_user) { soap.body = { :user_id => 123 } }
64
+ #
65
+ # # Prefixes the SOAP input tag with a given namespace: "<wsdl:GetUser>...</wsdl:GetUser>"
66
+ # client.request(:wsdl, "GetUser") { soap.body = { :user_id => 123 } }
67
+ #
68
+ # # SOAP input tag with attributes: <getUser xmlns:wsdl="http://example.com">...</getUser>"
69
+ # client.request(:get_user, "xmlns:wsdl" => "http://example.com")
70
+ def request(*args, &block)
71
+ raise ArgumentError, "Savon::Client#request requires at least one argument" if args.empty?
72
+
73
+ self.soap = SOAP::XML.new
74
+ preconfigure extract_options(args)
75
+ process &block if block
76
+ soap.wsse = wsse
77
+
78
+ SOAP::Request.new(http, soap).response
85
79
  end
86
80
 
87
81
  private
88
82
 
89
- # Dispatches requests to SOAP actions matching a given +method+ name.
90
- def method_missing(method, *args, &block) #:doc:
91
- soap_action = soap_action_from method.to_s
92
- super unless @wsdl.respond_to? soap_action
83
+ # Writer for the <tt>Savon::SOAP::XML</tt> object.
84
+ attr_writer :soap
93
85
 
94
- setup_objects *@wsdl.operation_from(soap_action), &block
95
- Response.new @request.soap(@soap)
86
+ # Accessor for the original self of a given block.
87
+ attr_accessor :original_self
88
+
89
+ # Expects an Array of +args+ and returns an Array containing the namespace (might be +nil+),
90
+ # the SOAP input and a Hash of attributes for the input tag (which might be empty).
91
+ def extract_options(args)
92
+ attributes = Hash === args.last ? args.pop : {}
93
+ namespace = args.size > 1 ? args.shift.to_sym : nil
94
+ input = args.first
95
+
96
+ [namespace, input, attributes]
96
97
  end
97
98
 
98
- # Sets whether to use Savon::WSDL by a given +method+ name and returns the original method name
99
- # without exclamation marks.
100
- def soap_action_from(method)
101
- @wsdl.enabled = !method.ends_with?("!")
99
+ # Expects and Array of +options+ and preconfigures the system.
100
+ def preconfigure(options)
101
+ soap.endpoint = wsdl.endpoint
102
+ soap.namespace_identifier = options[0]
103
+ soap.namespace = wsdl.namespace
104
+ soap.body = options[2].delete :body
105
+
106
+ set_soap_action options[1]
107
+ set_soap_input *options
108
+ end
109
+
110
+ # Expects an +input+ and sets the +SOAPAction+ HTTP headers.
111
+ def set_soap_action(input)
112
+ soap_action = wsdl.soap_action input.to_sym
113
+ soap_action ||= input.kind_of?(String) ? input : input.to_s.lower_camelcase
114
+ http.headers["SOAPAction"] = %{"#{soap_action}"}
115
+ end
116
+
117
+ # Expects a +namespace+, +input+ and +attributes+ and sets the SOAP input.
118
+ def set_soap_input(namespace, input, attributes)
119
+ new_input = wsdl.soap_input input.to_sym
120
+ new_input ||= input.kind_of?(String) ? input.to_sym : input.to_s.lower_camelcase.to_sym
121
+ soap.input = [namespace, new_input, attributes].compact
122
+ end
102
123
 
103
- method.chop! if method.ends_with?("!")
104
- method.to_sym
124
+ # Processes a given +block+. Yields objects if the block expects any arguments.
125
+ # Otherwise evaluates the block in the context of this object.
126
+ def process(offset = 0, &block)
127
+ block.arity > 0 ? yield_objects(offset, &block) : evaluate(&block)
105
128
  end
106
129
 
107
- # Returns the SOAP endpoint.
108
- def soap_endpoint
109
- @wsdl.enabled? ? @wsdl.soap_endpoint : @request.endpoint
130
+ # Yields a number of objects to a given +block+ depending on how many arguments
131
+ # the block is expecting.
132
+ def yield_objects(offset, &block)
133
+ yield *[soap, wsdl, http, wsse][offset, block.arity]
110
134
  end
111
135
 
112
- # Expects a SOAP operation Hash and sets up Savon::SOAP and Savon::WSSE. Yields them to a given
113
- # +block+ in case one was given.
114
- def setup_objects(action, input, &block)
115
- @soap, @wsse = SOAP.new(action, input, soap_endpoint), WSSE.new
116
- yield_objects &block if block
117
- @soap.namespaces["xmlns:wsdl"] ||= @wsdl.namespace_uri if @wsdl.enabled?
118
- @soap.wsse = @wsse
136
+ # Evaluates a given +block+ inside this object. Stores the original block binding.
137
+ def evaluate(&block)
138
+ self.original_self = eval "self", block.binding
139
+ instance_eval &block
119
140
  end
120
141
 
121
- # Yields either Savon::SOAP or Savon::SOAP and Savon::WSSE to a given +block+, depending on
122
- # the number of arguments expected by the block.
123
- def yield_objects(&block)
124
- case block.arity
125
- when 1 then yield @soap
126
- when 2 then yield @soap, @wsse
127
- end
142
+ # Handles calls to undefined methods by delegating to the original block binding.
143
+ def method_missing(method, *args, &block)
144
+ super unless original_self
145
+ original_self.send method, *args, &block
128
146
  end
129
147
 
130
148
  end
@@ -1,31 +1,45 @@
1
- class Array
1
+ require "builder"
2
2
 
3
- # Translates the Array into SOAP compatible XML. See: Hash.to_soap_xml.
4
- def to_soap_xml(key, escape_xml = true, attributes = {})
5
- xml = Builder::XmlMarkup.new
3
+ require "savon/core_ext/object"
4
+ require "savon/core_ext/string"
5
+ require "savon/core_ext/hash"
6
+ require "savon/core_ext/datetime"
6
7
 
7
- each_with_index do |item, index|
8
- attrs = tag_attributes attributes, index
9
- case item
10
- when Hash then xml.tag!(key, attrs) { xml << item.to_soap_xml }
11
- else xml.tag!(key, attrs) { xml << (escape_xml ? item.to_soap_value : item.to_soap_value!) }
12
- end
13
- end
8
+ module Savon
9
+ module CoreExt
10
+ module Array
14
11
 
15
- xml.target!
16
- end
12
+ # Translates the Array into SOAP compatible XML. See: Hash.to_soap_xml.
13
+ def to_soap_xml(key, escape_xml = true, attributes = {})
14
+ xml = Builder::XmlMarkup.new
15
+
16
+ each_with_index do |item, index|
17
+ attrs = tag_attributes attributes, index
18
+ case item
19
+ when ::Hash then xml.tag!(key, attrs) { xml << item.to_soap_xml }
20
+ when NilClass then xml.tag!(key, "xsi:nil" => "true")
21
+ else xml.tag!(key, attrs) { xml << (escape_xml ? item.to_soap_value : item.to_soap_value!) }
22
+ end
23
+ end
24
+
25
+ xml.target!
26
+ end
17
27
 
18
- private
28
+ private
19
29
 
20
- # Takes a Hash of +attributes+ and the +index+ for which to return attributes
21
- # for duplicate tags.
22
- def tag_attributes(attributes, index)
23
- return {} if attributes.empty?
30
+ # Takes a Hash of +attributes+ and the +index+ for which to return attributes
31
+ # for duplicate tags.
32
+ def tag_attributes(attributes, index)
33
+ return {} if attributes.empty?
34
+
35
+ attributes.inject({}) do |hash, (key, value)|
36
+ value = value[index] if value.kind_of? ::Array
37
+ hash.merge key => value
38
+ end
39
+ end
24
40
 
25
- attributes.inject({}) do |hash, (key, value)|
26
- value = value[index] if value.kind_of? Array
27
- hash.merge key => value
28
41
  end
29
42
  end
43
+ end
30
44
 
31
- end
45
+ Array.send :include, Savon::CoreExt::Array
@@ -1,10 +1,19 @@
1
- class DateTime
1
+ require "date"
2
+ require "savon/soap"
2
3
 
3
- # Returns the DateTime as an xs:dateTime formatted String.
4
- def to_soap_value
5
- strftime Savon::SOAP::DateTimeFormat
6
- end
4
+ module Savon
5
+ module CoreExt
6
+ module DateTime
7
+
8
+ # Returns the DateTime as an xs:dateTime formatted String.
9
+ def to_soap_value
10
+ strftime Savon::SOAP::DateTimeFormat
11
+ end
7
12
 
8
- alias_method :to_soap_value!, :to_soap_value
13
+ alias_method :to_soap_value!, :to_soap_value
9
14
 
15
+ end
16
+ end
10
17
  end
18
+
19
+ DateTime.send :include, Savon::CoreExt::DateTime
@@ -1,107 +1,135 @@
1
- class Hash
1
+ require "builder"
2
2
 
3
- # Returns the values from the soap:Body element or an empty Hash in case the soap:Body tag could
4
- # not be found.
5
- def find_soap_body
6
- envelope = self[keys.first] || {}
7
- body_key = envelope.keys.find { |key| /.+:Body/ =~ key } rescue nil
8
- body_key ? envelope[body_key].map_soap_response : {}
9
- end
10
-
11
- # Translates the Hash into SOAP request compatible XML.
12
- #
13
- # { :find_user => { :id => 123, "wsdl:Key" => "api" } }.to_soap_xml
14
- # # => "<findUser><id>123</id><wsdl:Key>api</wsdl:Key></findUser>"
15
- #
16
- # ==== Mapping
17
- #
18
- # * Hash keys specified as Symbols are converted to lowerCamelCase Strings
19
- # * Hash keys specified as Strings are not converted and may contain namespaces
20
- # * DateTime values are converted to xs:dateTime Strings
21
- # * Objects responding to to_datetime (except Strings) are converted to xs:dateTime Strings
22
- # * TrueClass and FalseClass objects are converted to "true" and "false" Strings
23
- # * All other objects are expected to be converted to Strings using to_s
24
- #
25
- # An example:
26
- #
27
- # { :magic_request => {
28
- # :perform_move => true,
29
- # "perform_at" => DateTime.new(2010, 11, 22, 11, 22, 33)
30
- # }
31
- # }.to_soap_xml
32
- #
33
- # <magicRequest>
34
- # <performMove>true</performMove>
35
- # <perform_at>2012-06-11T10:42:21</perform_at>
36
- # </magicRequest>
37
- #
38
- # ==== Escaped XML values
39
- #
40
- # By default, special characters in XML String values are escaped.
41
- #
42
- # ==== Fixed order of XML tags
43
- #
44
- # In case your service requires the tags to be in a specific order (parameterOrder), you have two
45
- # options. The first is to specify your body as an XML string. The second is to specify the order
46
- # through an additional array stored under the +:order!+ key.
47
- #
48
- # { :name => "Eve", :id => 123, :order! => [:id, :name] }.to_soap_xml
49
- # # => "<id>123</id><name>Eve</name>"
50
- #
51
- # ==== XML attributes
52
- #
53
- # If you need attributes, you could either go with an XML string or add another hash under the
54
- # +:attributes!+ key.
55
- #
56
- # { :person => "Eve", :attributes! => { :person => { :id => 666 } } }.to_soap_xml
57
- # # => '<person id="666">Eve</person>'
58
- def to_soap_xml
59
- xml = Builder::XmlMarkup.new
60
- attributes = delete(:attributes!) || {}
3
+ require "savon"
4
+ require "savon/core_ext/object"
5
+ require "savon/core_ext/string"
6
+ require "savon/core_ext/symbol"
7
+ require "savon/core_ext/array"
8
+ require "savon/core_ext/datetime"
61
9
 
62
- order.each do |key|
63
- attrs = attributes[key] || {}
64
- value = self[key]
65
- escape_xml = key.to_s[-1, 1] != "!"
66
- key = key.to_soap_key
10
+ module Savon
11
+ module CoreExt
12
+ module Hash
67
13
 
68
- case value
69
- when Array then xml << value.to_soap_xml(key, escape_xml, attrs)
70
- when Hash then xml.tag!(key, attrs) { xml << value.to_soap_xml }
71
- else xml.tag!(key, attrs) { xml << (escape_xml ? value.to_soap_value : value.to_soap_value!) }
14
+ # Returns the values from the soap:Body element or an empty Hash in case the soap:Body tag could
15
+ # not be found.
16
+ def find_soap_body
17
+ envelope = self[keys.first] || {}
18
+ body_key = envelope.keys.find { |key| /.+:Body/ =~ key } rescue nil
19
+ body_key ? envelope[body_key].map_soap_response : {}
72
20
  end
73
- end
74
21
 
75
- xml.target!
76
- end
22
+ # Translates the Hash into SOAP request compatible XML.
23
+ #
24
+ # { :find_user => { :id => 123, "wsdl:Key" => "api" } }.to_soap_xml
25
+ # # => "<findUser><id>123</id><wsdl:Key>api</wsdl:Key></findUser>"
26
+ #
27
+ # ==== Mapping
28
+ #
29
+ # * Hash keys specified as Symbols are converted to lowerCamelCase Strings
30
+ # * Hash keys specified as Strings are not converted and may contain namespaces
31
+ # * DateTime values are converted to xs:dateTime Strings
32
+ # * Objects responding to to_datetime (except Strings) are converted to xs:dateTime Strings
33
+ # * TrueClass and FalseClass objects are converted to "true" and "false" Strings
34
+ # * All other objects are expected to be converted to Strings using to_s
35
+ #
36
+ # An example:
37
+ #
38
+ # { :magic_request => {
39
+ # :perform_move => true,
40
+ # "perform_at" => DateTime.new(2010, 11, 22, 11, 22, 33)
41
+ # }
42
+ # }.to_soap_xml
43
+ #
44
+ # <magicRequest>
45
+ # <performMove>true</performMove>
46
+ # <perform_at>2012-06-11T10:42:21</perform_at>
47
+ # </magicRequest>
48
+ #
49
+ # ==== Escaped XML values
50
+ #
51
+ # By default, special characters in XML String values are escaped.
52
+ #
53
+ # ==== Fixed order of XML tags
54
+ #
55
+ # In case your service requires the tags to be in a specific order (parameterOrder), you have two
56
+ # options. The first is to specify your body as an XML string. The second is to specify the order
57
+ # through an additional array stored under the +:order!+ key.
58
+ #
59
+ # { :name => "Eve", :id => 123, :order! => [:id, :name] }.to_soap_xml
60
+ # # => "<id>123</id><name>Eve</name>"
61
+ #
62
+ # ==== XML attributes
63
+ #
64
+ # If you need attributes, you could either go with an XML string or add another hash under the
65
+ # +:attributes!+ key.
66
+ #
67
+ # { :person => "Eve", :attributes! => { :person => { :id => 666 } } }.to_soap_xml
68
+ # # => '<person id="666">Eve</person>'
69
+ def to_soap_xml
70
+ xml = Builder::XmlMarkup.new
71
+ attributes = delete(:attributes!) || {}
72
+
73
+ order.each do |key|
74
+ attrs = attributes[key] || {}
75
+ value = self[key]
76
+ escape_xml = key.to_s[-1, 1] != "!"
77
+ key = key.to_soap_key
78
+
79
+ case value
80
+ when ::Array then xml << value.to_soap_xml(key, escape_xml, attrs)
81
+ when ::Hash then xml.tag!(key, attrs) { xml << value.to_soap_xml }
82
+ when NilClass then xml.tag!(key, "xsi:nil" => "true")
83
+ else xml.tag!(key, attrs) { xml << (escape_xml ? value.to_soap_value : value.to_soap_value!) }
84
+ end
85
+ end
77
86
 
78
- # Maps keys and values of a Hash created from SOAP response XML to more convenient Ruby Objects.
79
- def map_soap_response
80
- inject({}) do |hash, (key, value)|
81
- value = case value
82
- when Hash then value["xsi:nil"] ? nil : value.map_soap_response
83
- when Array then value.map { |val| val.map_soap_response rescue val }
84
- when String then value.map_soap_response
87
+ xml.target!
85
88
  end
86
89
 
87
- hash.merge key.strip_namespace.snakecase.to_sym => value
88
- end
89
- end
90
-
91
- private
90
+ # Maps keys and values of a Hash created from SOAP response XML to more convenient Ruby Objects.
91
+ def map_soap_response
92
+ inject({}) do |hash, (key, value)|
93
+ value = case value
94
+ when ::Hash then value["xsi:nil"] ? nil : value.map_soap_response
95
+ when ::Array then value.map { |val| val.map_soap_response rescue val }
96
+ when ::String then value.map_soap_response
97
+ end
98
+
99
+ new_key = if Savon.strip_namespaces?
100
+ key.strip_namespace.snakecase.to_sym
101
+ else
102
+ key.snakecase
103
+ end
104
+
105
+ if hash[new_key] # key already exists, value should be added as an Array
106
+ hash[new_key] = [hash[new_key], value].flatten
107
+ result = hash
108
+ else
109
+ result = hash.merge new_key => value
110
+ end
111
+ result
112
+ end
113
+ end
92
114
 
93
- # Deletes and returns an Array of keys stored under the :order! key. Defaults to return the actual
94
- # keys of this Hash if no :order! key could be found. Raises an ArgumentError in case the :order!
95
- # Array does not match the Hash keys.
96
- def order
97
- order = delete :order!
98
- order = keys unless order.kind_of? Array
115
+ private
99
116
 
100
- missing, spurious = keys - order, order - keys
101
- raise ArgumentError, "Missing elements in :order! #{missing.inspect}" unless missing.empty?
102
- raise ArgumentError, "Spurious elements in :order! #{spurious.inspect}" unless spurious.empty?
117
+ # Deletes and returns an Array of keys stored under the :order! key. Defaults to return the actual
118
+ # keys of this Hash if no :order! key could be found. Raises an ArgumentError in case the :order!
119
+ # Array does not match the Hash keys.
120
+ def order
121
+ order = delete :order!
122
+ order = keys unless order.kind_of? ::Array
123
+
124
+ missing, spurious = keys - order, order - keys
125
+ raise ArgumentError, "Missing elements in :order! #{missing.inspect}" unless missing.empty?
126
+ raise ArgumentError, "Spurious elements in :order! #{spurious.inspect}" unless spurious.empty?
127
+
128
+ order
129
+ end
103
130
 
104
- order
131
+ end
105
132
  end
133
+ end
106
134
 
107
- end
135
+ Hash.send :include, Savon::CoreExt::Hash