savon 2.2.0 → 2.12.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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.travis.yml +20 -9
  4. data/CHANGELOG.md +157 -10
  5. data/CONTRIBUTING.md +1 -1
  6. data/Gemfile +10 -2
  7. data/README.md +38 -13
  8. data/donate.png +0 -0
  9. data/lib/savon/builder.rb +81 -15
  10. data/lib/savon/client.rb +6 -2
  11. data/lib/savon/core_ext/string.rb +0 -1
  12. data/lib/savon/header.rb +68 -17
  13. data/lib/savon/log_message.rb +7 -3
  14. data/lib/savon/message.rb +6 -7
  15. data/lib/savon/mock/expectation.rb +12 -2
  16. data/lib/savon/model.rb +4 -0
  17. data/lib/savon/operation.rb +45 -38
  18. data/lib/savon/options.rb +149 -22
  19. data/lib/savon/qualified_message.rb +31 -25
  20. data/lib/savon/request.rb +24 -4
  21. data/lib/savon/request_logger.rb +48 -0
  22. data/lib/savon/response.rb +35 -18
  23. data/lib/savon/soap_fault.rb +11 -11
  24. data/lib/savon/version.rb +1 -3
  25. data/savon.gemspec +12 -11
  26. data/spec/fixtures/response/empty_soap_fault.xml +13 -0
  27. data/spec/fixtures/response/f5.xml +39 -0
  28. data/spec/fixtures/response/no_body.xml +1 -0
  29. data/spec/fixtures/response/soap_fault_funky.xml +8 -0
  30. data/spec/fixtures/wsdl/brand.xml +624 -0
  31. data/spec/fixtures/wsdl/elements_in_types.xml +43 -0
  32. data/spec/fixtures/wsdl/no_message_tag.xml +1267 -0
  33. data/spec/fixtures/wsdl/vies.xml +176 -0
  34. data/spec/integration/centra_spec.rb +67 -0
  35. data/spec/integration/email_example_spec.rb +1 -1
  36. data/spec/integration/random_quote_spec.rb +23 -0
  37. data/spec/integration/stockquote_example_spec.rb +7 -1
  38. data/spec/integration/support/application.rb +1 -1
  39. data/spec/integration/zipcode_example_spec.rb +1 -1
  40. data/spec/savon/builder_spec.rb +50 -0
  41. data/spec/savon/client_spec.rb +78 -0
  42. data/spec/savon/core_ext/string_spec.rb +9 -9
  43. data/spec/savon/features/message_tag_spec.rb +5 -0
  44. data/spec/savon/http_error_spec.rb +2 -2
  45. data/spec/savon/log_message_spec.rb +18 -1
  46. data/spec/savon/message_spec.rb +70 -0
  47. data/spec/savon/mock_spec.rb +31 -0
  48. data/spec/savon/model_spec.rb +28 -0
  49. data/spec/savon/operation_spec.rb +69 -3
  50. data/spec/savon/options_spec.rb +515 -87
  51. data/spec/savon/qualified_message_spec.rb +101 -0
  52. data/spec/savon/request_logger_spec.rb +37 -0
  53. data/spec/savon/request_spec.rb +85 -10
  54. data/spec/savon/response_spec.rb +118 -27
  55. data/spec/savon/soap_fault_spec.rb +25 -5
  56. data/spec/savon/softlayer_spec.rb +27 -0
  57. data/spec/spec_helper.rb +5 -2
  58. data/spec/support/adapters.rb +48 -0
  59. data/spec/support/integration.rb +1 -1
  60. metadata +76 -93
@@ -21,7 +21,7 @@ module Savon
21
21
  build_wsdl_document
22
22
  end
23
23
 
24
- attr_reader :globals
24
+ attr_reader :globals, :wsdl
25
25
 
26
26
  def operations
27
27
  raise_missing_wsdl_error! unless @wsdl.document?
@@ -41,6 +41,10 @@ module Savon
41
41
  @wsdl.service_name
42
42
  end
43
43
 
44
+ def build_request(operation_name, locals = {}, &block)
45
+ operation(operation_name).request(locals, &block)
46
+ end
47
+
44
48
  private
45
49
 
46
50
  def set_globals(globals, block)
@@ -56,7 +60,7 @@ module Savon
56
60
  @wsdl.document = @globals[:wsdl] if @globals.include? :wsdl
57
61
  @wsdl.endpoint = @globals[:endpoint] if @globals.include? :endpoint
58
62
  @wsdl.namespace = @globals[:namespace] if @globals.include? :namespace
59
- @wsdl.servicename = @globals[:servicename] if @globals.include? :servicename
63
+ @wsdl.adapter = @globals[:adapter] if @globals.include? :adapter
60
64
 
61
65
  @wsdl.request = WSDLRequest.new(@globals).build
62
66
  end
@@ -1,4 +1,3 @@
1
- require "savon/soap"
2
1
 
3
2
  module Savon
4
3
  module CoreExt
@@ -1,41 +1,92 @@
1
1
  require "akami"
2
2
  require "gyoku"
3
+ require "securerandom"
3
4
 
4
5
  module Savon
5
6
  class Header
6
7
 
7
8
  def initialize(globals, locals)
8
- @globals = globals
9
- @locals = locals
10
- @wsse = create_wsse
9
+ @gyoku_options = { :key_converter => globals[:convert_request_keys_to] }
10
+
11
+ @wsse_auth = locals[:wsse_auth].nil? ? globals[:wsse_auth] : locals[:wsse_auth]
12
+ @wsse_timestamp = locals[:wsse_timestamp].nil? ? globals[:wsse_timestamp] : locals[:wsse_timestamp]
13
+ @wsse_signature = locals[:wsse_signature].nil? ? globals[:wsse_signature] : locals[:wsse_signature]
14
+
15
+ @global_header = globals[:soap_header]
16
+ @local_header = locals[:soap_header]
17
+
18
+ @globals = globals
19
+ @locals = locals
20
+
21
+ @header = build
11
22
  end
12
23
 
24
+ attr_reader :local_header, :global_header, :gyoku_options,
25
+ :wsse_auth, :wsse_timestamp, :wsse_signature
26
+
13
27
  def empty?
14
- to_s.empty?
28
+ @header.empty?
15
29
  end
16
30
 
17
31
  def to_s
18
- return @header if @header
19
-
20
- gyoku_options = { :key_converter => @globals[:convert_request_keys_to] }
21
- @header = (Hash === header ? Gyoku.xml(header, gyoku_options) : header) + wsse_header
32
+ @header
22
33
  end
23
34
 
24
35
  private
25
36
 
26
- def create_wsse
27
- wsse = Akami.wsse
28
- wsse.credentials(*@globals[:wsse_auth]) if @globals.include? :wsse_auth
29
- wsse.timestamp = @globals[:wsse_timestamp] if @globals.include? :wsse_timestamp
30
- wsse
37
+ def build
38
+ build_header + build_wsa_header + build_wsse_header
39
+ end
40
+
41
+ def build_header
42
+ header =
43
+ if global_header.kind_of?(Hash) && local_header.kind_of?(Hash)
44
+ global_header.merge(local_header)
45
+ elsif local_header
46
+ local_header
47
+ else
48
+ global_header
49
+ end
50
+
51
+ convert_to_xml(header)
31
52
  end
32
53
 
33
- def header
34
- @header ||= @globals.include?(:soap_header) ? @globals[:soap_header] : {}
54
+ def build_wsse_header
55
+ wsse_header = akami
56
+ wsse_header.respond_to?(:to_xml) ? wsse_header.to_xml : ""
35
57
  end
36
58
 
37
- def wsse_header
38
- @wsse.respond_to?(:to_xml) ? @wsse.to_xml : ""
59
+ def build_wsa_header
60
+ return '' unless @globals[:use_wsa_headers]
61
+ convert_to_xml({
62
+ 'wsa:Action' => @locals[:soap_action],
63
+ 'wsa:To' => @globals[:endpoint],
64
+ 'wsa:MessageID' => "urn:uuid:#{SecureRandom.uuid}",
65
+ attributes!: {
66
+ 'wsa:MessageID' => {
67
+ "xmlns:wsa" => "http://schemas.xmlsoap.org/ws/2004/08/addressing"
68
+ }
69
+ }
70
+ })
71
+ end
72
+
73
+ def convert_to_xml(hash_or_string)
74
+ if hash_or_string.kind_of? Hash
75
+ Gyoku.xml(hash_or_string, gyoku_options)
76
+ else
77
+ hash_or_string.to_s
78
+ end
79
+ end
80
+
81
+ def akami
82
+ wsse = Akami.wsse
83
+ wsse.credentials(*wsse_auth) if wsse_auth
84
+ wsse.timestamp = wsse_timestamp if wsse_timestamp
85
+ if wsse_signature && wsse_signature.have_document?
86
+ wsse.signature = wsse_signature
87
+ end
88
+
89
+ wsse
39
90
  end
40
91
 
41
92
  end
@@ -35,13 +35,17 @@ module Savon
35
35
  end
36
36
 
37
37
  def apply_filter!(document, filter)
38
- document.xpath("//*[local-name()='#{filter}']").each do |node|
39
- node.content = "***FILTERED***"
38
+ if filter.instance_of? Proc
39
+ filter.call document
40
+ else
41
+ document.xpath("//*[local-name()='#{filter}']").each do |node|
42
+ node.content = "***FILTERED***"
43
+ end
40
44
  end
41
45
  end
42
46
 
43
47
  def nokogiri_options
44
- @pretty_print ? { :indent => 2 } : {}
48
+ @pretty_print ? { :indent => 2 } : { :save_with => Nokogiri::XML::Node::SaveOptions::AS_XML }
45
49
  end
46
50
 
47
51
  end
@@ -4,8 +4,8 @@ require "gyoku"
4
4
  module Savon
5
5
  class Message
6
6
 
7
- def initialize(operation_name, namespace_identifier, types, used_namespaces, message, element_form_default, key_converter)
8
- @operation_name = operation_name
7
+ def initialize(message_tag, namespace_identifier, types, used_namespaces, message, element_form_default, key_converter, unwrap)
8
+ @message_tag = message_tag
9
9
  @namespace_identifier = namespace_identifier
10
10
  @types = types
11
11
  @used_namespaces = used_namespaces
@@ -13,22 +13,21 @@ module Savon
13
13
  @message = message
14
14
  @element_form_default = element_form_default
15
15
  @key_converter = key_converter
16
+ @unwrap = unwrap
16
17
  end
17
18
 
18
19
  def to_s
19
20
  return @message.to_s unless @message.kind_of? Hash
20
21
 
21
22
  if @element_form_default == :qualified
22
- translated_operation_name = Gyoku.xml_tag(@operation_name, :key_converter => @key_converter).to_s
23
- # XXX: there is no `@request_key_converter` instance variable!
24
- # the third argument is therefore always `nil`. [dh, 2013-03-09]
25
- @message = QualifiedMessage.new(@types, @used_namespaces, @request_key_converter).to_hash(@message, [translated_operation_name])
23
+ @message = QualifiedMessage.new(@types, @used_namespaces, @key_converter).to_hash(@message, [@message_tag.to_s])
26
24
  end
27
25
 
28
26
  gyoku_options = {
29
27
  :element_form_default => @element_form_default,
30
28
  :namespace => @namespace_identifier,
31
- :key_converter => @key_converter
29
+ :key_converter => @key_converter,
30
+ :unwrap => @unwrap
32
31
  }
33
32
 
34
33
  Gyoku.xml(@message, gyoku_options)
@@ -54,7 +54,8 @@ module Savon
54
54
  end
55
55
 
56
56
  def verify_message!
57
- unless @expected[:message] == @actual[:message]
57
+ return if @expected[:message].eql? :any
58
+ unless equals_except_any(@expected[:message], @actual[:message])
58
59
  expected_message = " with this message: #{@expected[:message].inspect}" if @expected[:message]
59
60
  expected_message ||= " with no message."
60
61
 
@@ -62,9 +63,18 @@ module Savon
62
63
  actual_message ||= " with no message."
63
64
 
64
65
  raise ExpectationError, "Expected a request to the #{@expected[:operation_name].inspect} operation\n#{expected_message}\n" \
65
- "Received a request to the #{@actual[:operation_name].inspect} operation\n#{actual_message}"
66
+ "Received a request to the #{@actual[:operation_name].inspect} operation\n#{actual_message}"
66
67
  end
67
68
  end
68
69
 
70
+ def equals_except_any(msg_expected, msg_real)
71
+ return true if msg_expected === msg_real
72
+ return false if (msg_expected.nil? || msg_real.nil?) # If both are nil has returned true
73
+ msg_expected.each do |key, expected_value|
74
+ next if (expected_value == :any && msg_real.include?(key))
75
+ return false if expected_value != msg_real[key]
76
+ end
77
+ return true
78
+ end
69
79
  end
70
80
  end
@@ -19,6 +19,10 @@ module Savon
19
19
  end
20
20
  end
21
21
 
22
+ def all_operations
23
+ operations(*client.operations)
24
+ end
25
+
22
26
  private
23
27
 
24
28
  # Defines a class-level SOAP operation.
@@ -3,7 +3,8 @@ require "savon/block_interface"
3
3
  require "savon/request"
4
4
  require "savon/builder"
5
5
  require "savon/response"
6
- require "savon/log_message"
6
+ require "savon/request_logger"
7
+ require "savon/http_error"
7
8
 
8
9
  module Savon
9
10
  class Operation
@@ -22,6 +23,8 @@ module Savon
22
23
  raise UnknownOperationError, "Unable to find SOAP operation: #{operation_name.inspect}\n" \
23
24
  "Operations provided by your service: #{wsdl.soap_actions.inspect}"
24
25
  end
26
+ rescue Wasabi::Resolver::HTTPError => e
27
+ raise HTTPError.new(e.response)
25
28
  end
26
29
 
27
30
  def self.ensure_name_is_symbol!(operation_name)
@@ -35,6 +38,8 @@ module Savon
35
38
  @name = name
36
39
  @wsdl = wsdl
37
40
  @globals = globals
41
+
42
+ @logger = RequestLogger.new(globals)
38
43
  end
39
44
 
40
45
  def build(locals = {}, &block)
@@ -46,15 +51,38 @@ module Savon
46
51
  builder = build(locals, &block)
47
52
 
48
53
  response = Savon.notify_observers(@name, builder, @globals, @locals)
49
- response ||= call! build_request(builder)
54
+ response ||= call_with_logging build_request(builder)
50
55
 
51
56
  raise_expected_httpi_response! unless response.kind_of?(HTTPI::Response)
52
57
 
53
- Response.new(response, @globals, @locals)
58
+ create_response(response)
59
+ end
60
+
61
+ def request(locals = {}, &block)
62
+ builder = build(locals, &block)
63
+ build_request(builder)
54
64
  end
55
65
 
56
66
  private
57
67
 
68
+ def create_response(response)
69
+ if multipart_supported?
70
+ Multipart::Response.new(response, @globals, @locals)
71
+ else
72
+ Response.new(response, @globals, @locals)
73
+ end
74
+ end
75
+
76
+ def multipart_supported?
77
+ return false unless @globals[:multipart] || @locals[:multipart]
78
+
79
+ if Savon.const_defined? :Multipart
80
+ true
81
+ else
82
+ raise 'Unable to find Savon::Multipart. Make sure the savon-multipart gem is installed and loaded.'
83
+ end
84
+ end
85
+
58
86
  def set_locals(locals, block)
59
87
  locals = LocalOptions.new(locals)
60
88
  BlockInterface.new(locals).evaluate(block) if block
@@ -62,18 +90,18 @@ module Savon
62
90
  @locals = locals
63
91
  end
64
92
 
65
- def call!(request)
66
- log_request(request) if log?
67
- response = HTTPI.post(request)
68
- log_response(response) if log?
69
-
70
- response
93
+ def call_with_logging(request)
94
+ @logger.log(request) { HTTPI.post(request, @globals[:adapter]) }
71
95
  end
72
96
 
73
97
  def build_request(builder)
98
+ @locals[:soap_action] ||= soap_action
99
+ @globals[:endpoint] ||= endpoint
100
+
74
101
  request = SOAPRequest.new(@globals).build(
75
102
  :soap_action => soap_action,
76
- :cookies => @locals[:cookies]
103
+ :cookies => @locals[:cookies],
104
+ :headers => @locals[:headers]
77
105
  )
78
106
 
79
107
  request.url = endpoint
@@ -99,34 +127,13 @@ module Savon
99
127
  end
100
128
 
101
129
  def endpoint
102
- @globals[:endpoint] || @wsdl.endpoint
103
- end
104
-
105
- def log_request(request)
106
- logger.info "SOAP request: #{request.url}"
107
- logger.info headers_to_log(request.headers)
108
- logger.debug body_to_log(request.body)
109
- end
110
-
111
- def log_response(response)
112
- logger.info "SOAP response (status #{response.code})"
113
- logger.debug body_to_log(response.body)
114
- end
115
-
116
- def headers_to_log(headers)
117
- headers.map { |key, value| "#{key}: #{value}" }.join(", ")
118
- end
119
-
120
- def body_to_log(body)
121
- LogMessage.new(body, @globals[:filters], @globals[:pretty_print_xml]).to_s
122
- end
123
-
124
- def logger
125
- @globals[:logger]
126
- end
127
-
128
- def log?
129
- @globals[:log]
130
+ @globals[:endpoint] || @wsdl.endpoint.tap do |url|
131
+ if @globals[:host]
132
+ host_url = URI.parse(@globals[:host])
133
+ url.host = host_url.host
134
+ url.port = host_url.port
135
+ end
136
+ end
130
137
  end
131
138
 
132
139
  def raise_expected_httpi_response!
@@ -35,25 +35,63 @@ module Savon
35
35
  def method_missing(option, _)
36
36
  raise UnknownOptionError, "Unknown #{option_type} option: #{option.inspect}"
37
37
  end
38
+ end
39
+
40
+ module SharedOptions
41
+ # WSSE auth credentials for Akami.
42
+ # Local will override the global wsse_auth value, e.g.
43
+ # global == [user, pass] && local == [user2, pass2] => [user2, pass2]
44
+ # global == [user, pass] && local == false => false
45
+ # global == [user, pass] && local == nil => [user, pass]
46
+ def wsse_auth(*credentials)
47
+ credentials.flatten!
48
+ if credentials.size == 1
49
+ @options[:wsse_auth] = credentials.first
50
+ else
51
+ @options[:wsse_auth] = credentials
52
+ end
53
+ end
54
+
55
+ # Instruct Akami to enable wsu:Timestamp headers.
56
+ # Local will override the global wsse_timestamp value, e.g.
57
+ # global == true && local == true => true
58
+ # global == true && local == false => false
59
+ # global == true && local == nil => true
60
+ def wsse_timestamp(timestamp = true)
61
+ @options[:wsse_timestamp] = timestamp
62
+ end
38
63
 
64
+ def wsse_signature(signature)
65
+ @options[:wsse_signature] = signature
66
+ end
39
67
  end
40
68
 
41
69
  class GlobalOptions < Options
70
+ include SharedOptions
42
71
 
43
72
  def initialize(options = {})
44
73
  @option_type = :global
45
74
 
46
75
  defaults = {
47
- :encoding => "UTF-8",
48
- :soap_version => 1,
49
- :namespaces => {},
50
- :logger => Logger.new($stdout),
51
- :log => true,
52
- :filters => [],
53
- :pretty_print_xml => false,
54
- :raise_errors => true,
55
- :strip_namespaces => true,
56
- :convert_response_tags_to => lambda { |tag| tag.snakecase.to_sym }
76
+ :encoding => "UTF-8",
77
+ :soap_version => 1,
78
+ :namespaces => {},
79
+ :logger => Logger.new($stdout),
80
+ :log => false,
81
+ :filters => [],
82
+ :pretty_print_xml => false,
83
+ :raise_errors => true,
84
+ :strip_namespaces => true,
85
+ :delete_namespace_attributes => false,
86
+ :convert_response_tags_to => lambda { |tag| tag.snakecase.to_sym},
87
+ :convert_attributes_to => lambda { |k,v| [k,v] },
88
+ :multipart => false,
89
+ :adapter => nil,
90
+ :use_wsa_headers => false,
91
+ :no_message_tag => false,
92
+ :follow_redirects => false,
93
+ :unwrap => false,
94
+ :host => nil
57
95
  }
58
96
 
59
97
  options = defaults.merge(options)
@@ -72,6 +110,11 @@ module Savon
72
110
  @options[:wsdl] = wsdl_address
73
111
  end
74
112
 
113
+ # set different host for actions in WSDL
114
+ def host(host)
115
+ @options[:host] = host
116
+ end
117
+
75
118
  # SOAP endpoint.
76
119
  def endpoint(endpoint)
77
120
  @options[:endpoint] = endpoint
@@ -94,7 +137,7 @@ module Savon
94
137
 
95
138
  # Proxy server to use for all requests.
96
139
  def proxy(proxy)
97
- @options[:proxy] = proxy
140
+ @options[:proxy] = proxy unless proxy.nil?
98
141
  end
99
142
 
100
143
  # A Hash of HTTP headers.
@@ -117,12 +160,12 @@ module Savon
117
160
  @options[:encoding] = encoding
118
161
  end
119
162
 
120
- # The global SOAP header. Expected to be a Hash.
163
+ # The global SOAP header. Expected to be a Hash or responding to #to_s.
121
164
  def soap_header(header)
122
165
  @options[:soap_header] = header
123
166
  end
124
167
 
125
- # Sets whether elements should be :qualified or unqualified.
168
+ # Sets whether elements should be :qualified or :unqualified.
126
169
  # If you need to use this option, please open an issue and make
127
170
  # sure to add your WSDL document for debugging.
128
171
  def element_form_default(element_form_default)
@@ -154,6 +197,7 @@ module Savon
154
197
 
155
198
  # The logger to use. Defaults to a Savon::Logger instance.
156
199
  def logger(logger)
200
+ HTTPI.logger = logger
157
201
  @options[:logger] = logger
158
202
  end
159
203
 
@@ -194,6 +238,11 @@ module Savon
194
238
  @options[:ssl_cert_key_file] = file
195
239
  end
196
240
 
241
+ # Sets the cert key to use.
242
+ def ssl_cert_key(key)
243
+ @options[:ssl_cert_key] = key
244
+ end
245
+
197
246
  # Sets the cert key password to use.
198
247
  def ssl_cert_key_password(password)
199
248
  @options[:ssl_cert_key_password] = password
@@ -204,11 +253,35 @@ module Savon
204
253
  @options[:ssl_cert_file] = file
205
254
  end
206
255
 
256
+ # Sets the cert to use.
257
+ def ssl_cert(cert)
258
+ @options[:ssl_cert] = cert
259
+ end
260
+
207
261
  # Sets the ca cert file to use.
208
262
  def ssl_ca_cert_file(file)
209
263
  @options[:ssl_ca_cert_file] = file
210
264
  end
211
265
 
266
+ # Sets the ca cert to use.
267
+ def ssl_ca_cert(cert)
268
+ @options[:ssl_ca_cert] = cert
269
+ end
270
+
271
+ def ssl_ciphers(ciphers)
272
+ @options[:ssl_ciphers] = ciphers
273
+ end
274
+
275
+ # Sets the ca cert path.
276
+ def ssl_ca_cert_path(path)
277
+ @options[:ssl_ca_cert_path] = path
278
+ end
279
+
280
+ # Sets the ssl cert store.
281
+ def ssl_cert_store(store)
282
+ @options[:ssl_cert_store] = store
283
+ end
284
+
212
285
  # HTTP basic auth credentials.
213
286
  def basic_auth(*credentials)
214
287
  @options[:basic_auth] = credentials.flatten
@@ -219,14 +292,9 @@ module Savon
219
292
  @options[:digest_auth] = credentials.flatten
220
293
  end
221
294
 
222
- # WSSE auth credentials for Akami.
223
- def wsse_auth(*credentials)
224
- @options[:wsse_auth] = credentials.flatten
225
- end
226
-
227
- # Instruct Akami to enable wsu:Timestamp headers.
228
- def wsse_timestamp(*timestamp)
229
- @options[:wsse_timestamp] = timestamp.flatten
295
+ # NTLM auth credentials.
296
+ def ntlm(*credentials)
297
+ @options[:ntlm] = credentials.flatten
230
298
  end
231
299
 
232
300
  # Instruct Nori whether to strip namespaces from XML nodes.
@@ -234,33 +302,84 @@ module Savon
234
302
  @options[:strip_namespaces] = strip_namespaces
235
303
  end
236
304
 
305
+ # Instruct Nori whether to delete namespace attributes from XML nodes.
306
+ def delete_namespace_attributes(delete_namespace_attributes)
307
+ @options[:delete_namespace_attributes] = delete_namespace_attributes
308
+ end
309
+
237
310
  # Tell Gyoku how to convert Hash key Symbols to XML tags.
238
311
  # Accepts one of :lower_camelcase, :camelcase, :upcase, or :none.
239
312
  def convert_request_keys_to(converter)
240
313
  @options[:convert_request_keys_to] = converter
241
314
  end
242
315
 
316
+ # Tell Gyoku to unwrap Array of Hashes
317
+ # Accepts a boolean, default to false
318
+ def unwrap(unwrap)
319
+ @options[:unwrap] = unwrap
320
+ end
321
+
243
322
  # Tell Nori how to convert XML tags from the SOAP response into Hash keys.
244
323
  # Accepts a lambda or a block which receives an XML tag and returns a Hash key.
245
324
  # Defaults to convert tags to snakecase Symbols.
246
325
  def convert_response_tags_to(converter = nil, &block)
247
326
  @options[:convert_response_tags_to] = block || converter
248
327
  end
328
+
329
+ # Tell Nori how to convert XML attributes on tags from the SOAP response into Hash keys.
330
+ # Accepts a lambda or a block which receives an XML tag and returns a Hash key.
331
+ # Defaults to doing nothing
332
+ def convert_attributes_to(converter = nil, &block)
333
+ @options[:convert_attributes_to] = block || converter
334
+ end
335
+
336
+ # Instruct Savon to create a multipart response if available.
337
+ def multipart(multipart)
338
+ @options[:multipart] = multipart
339
+ end
340
+
341
+ # Instruct Savon what HTTPI adapter it should use instead of default
342
+ def adapter(adapter)
343
+ @options[:adapter] = adapter
344
+ end
345
+
346
+ # Enable inclusion of WS-Addressing headers.
347
+ def use_wsa_headers(use)
348
+ @options[:use_wsa_headers] = use
349
+ end
350
+
351
+ def no_message_tag(bool)
352
+ @options[:no_message_tag] = bool
353
+ end
354
+
355
+ # Instruct requests to follow HTTP redirects.
356
+ def follow_redirects(follow_redirects)
357
+ @options[:follow_redirects] = follow_redirects
358
+ end
249
359
  end
250
360
 
251
361
  class LocalOptions < Options
362
+ include SharedOptions
252
363
 
253
364
  def initialize(options = {})
254
365
  @option_type = :local
255
366
 
256
367
  defaults = {
257
368
  :advanced_typecasting => true,
258
- :response_parser => :nokogiri
369
+ :response_parser => :nokogiri,
370
+ :multipart => false
259
371
  }
260
372
 
261
373
  super defaults.merge(options)
262
374
  end
263
375
 
376
+ # The local SOAP header. Expected to be a Hash or respond to #to_s.
377
+ # Will be merged with the global SOAP header if both are Hashes.
378
+ # Otherwise the local option will be prefered.
379
+ def soap_header(header)
380
+ @options[:soap_header] = header
381
+ end
382
+
264
383
  # The SOAP message to send. Expected to be a Hash or a String.
265
384
  def message(message)
266
385
  @options[:message] = message
@@ -302,5 +421,13 @@ module Savon
302
421
  @options[:response_parser] = parser
303
422
  end
304
423
 
424
+ # Instruct Savon to create a multipart response if available.
425
+ def multipart(multipart)
426
+ @options[:multipart] = multipart
427
+ end
428
+
429
+ def headers(headers)
430
+ @options[:headers] = headers
431
+ end
305
432
  end
306
433
  end