savon 0.7.9 → 0.8.0.beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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