savon 2.2.0 → 2.12.1

Sign up to get free protection for your applications and to get access to all the features.
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