savon 2.16.0 → 2.17.4

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.
data/lib/savon/options.rb CHANGED
@@ -1,10 +1,14 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "logger"
3
4
  require "httpi"
5
+ require "savon/faraday_migration_hint"
4
6
 
5
7
  module Savon
8
+ # Base class for GlobalOptions and LocalOptions.
9
+ # Stores options in a hash, dispatches setter calls by method name,
10
+ # raises UnknownOptionError for anything not defined on the subclass.
6
11
  class Options
7
-
8
12
  def initialize(options = {})
9
13
  @options = {}
10
14
  assign options
@@ -18,7 +22,7 @@ module Savon
18
22
 
19
23
  def []=(option, value)
20
24
  value = [value].flatten
21
- self.send(option, *value)
25
+ send(option, *value)
22
26
  end
23
27
 
24
28
  def include?(option)
@@ -29,7 +33,7 @@ module Savon
29
33
 
30
34
  def assign(options)
31
35
  options.each do |option, value|
32
- self.send(option, value)
36
+ send(option, value)
33
37
  end
34
38
  end
35
39
 
@@ -38,6 +42,8 @@ module Savon
38
42
  end
39
43
  end
40
44
 
45
+ # Options available in both GlobalOptions and LocalOptions.
46
+ # Currently covers WSSE authentication and timestamp headers.
41
47
  module SharedOptions
42
48
  # WSSE auth credentials for Akami.
43
49
  # Local will override the global wsse_auth value, e.g.
@@ -46,11 +52,12 @@ module Savon
46
52
  # global == [user, pass] && local == nil => [user, pass]
47
53
  def wsse_auth(*credentials)
48
54
  credentials.flatten!
49
- if credentials.size == 1
50
- @options[:wsse_auth] = credentials.first
51
- else
52
- @options[:wsse_auth] = credentials
53
- end
55
+ @options[:wsse_auth] =
56
+ if credentials.size == 1
57
+ credentials.first
58
+ else
59
+ credentials
60
+ end
54
61
  end
55
62
 
56
63
  # Instruct Akami to enable wsu:Timestamp headers.
@@ -67,35 +74,186 @@ module Savon
67
74
  end
68
75
  end
69
76
 
77
+ # Options that belong to HTTPI's transport layer.
78
+ #
79
+ # They still work with the default HTTPI transport. With `transport: :faraday`,
80
+ # Savon does not translate them because Faraday exposes its own setup API for
81
+ # proxy, timeout, TLS, auth, redirects, and adapter choices.
82
+ module HTTPITransportOptions
83
+ TRANSPORT_DEFAULTS = {
84
+ adapter: nil,
85
+ follow_redirects: false
86
+ }.freeze
87
+
88
+ # These are rejected for `transport: :faraday` once the caller has set a
89
+ # non-default value. The error points to the matching Faraday setup.
90
+ FARADAY_INCOMPATIBLE_GLOBALS = FaradayMigrationHint::OPTIONS
91
+
92
+ # Runs after all global options have been assigned, including options set in
93
+ # the client block. Reports every HTTPI-only option in one error so callers
94
+ # can move their transport setup to Faraday in one pass.
95
+ def validate_transport!
96
+ return unless self[:transport] == :faraday
97
+
98
+ unless faraday_loaded?
99
+ raise InitializationError,
100
+ "transport: :faraday requires the faraday gem.\n" \
101
+ "Add to your Gemfile: gem 'faraday'"
102
+ end
103
+
104
+ violations = FARADAY_INCOMPATIBLE_GLOBALS.filter_map { |option|
105
+ next unless include?(option)
106
+ next if default_transport_option?(option)
107
+
108
+ FaradayMigrationHint.new(option, self[option]).message
109
+ }
110
+
111
+ return if violations.empty?
112
+
113
+ raise InitializationError,
114
+ "The following options are not supported with transport: :faraday:\n#{violations.join("\n")}"
115
+ end
116
+
117
+ # Proxy server to use for all requests.
118
+ def proxy(proxy)
119
+ @options[:proxy] = proxy unless proxy.nil?
120
+ end
121
+
122
+ # Open timeout in seconds.
123
+ def open_timeout(open_timeout)
124
+ @options[:open_timeout] = open_timeout
125
+ end
126
+
127
+ # Read timeout in seconds.
128
+ def read_timeout(read_timeout)
129
+ @options[:read_timeout] = read_timeout
130
+ end
131
+
132
+ # Write timeout in seconds.
133
+ def write_timeout(write_timeout)
134
+ @options[:write_timeout] = write_timeout
135
+ end
136
+
137
+ # Specifies the SSL version to use.
138
+ def ssl_version(version)
139
+ @options[:ssl_version] = version
140
+ end
141
+
142
+ # Specifies the minimum SSL version to use.
143
+ def ssl_min_version(version)
144
+ @options[:ssl_min_version] = version
145
+ end
146
+
147
+ # Specifies the maximum SSL version to use.
148
+ def ssl_max_version(version)
149
+ @options[:ssl_max_version] = version
150
+ end
151
+
152
+ # Whether and how to verify the SSL connection.
153
+ def ssl_verify_mode(verify_mode)
154
+ @options[:ssl_verify_mode] = verify_mode
155
+ end
156
+
157
+ # Sets the cert key file to use.
158
+ def ssl_cert_key_file(file)
159
+ @options[:ssl_cert_key_file] = file
160
+ end
161
+
162
+ # Sets the cert key to use.
163
+ def ssl_cert_key(key)
164
+ @options[:ssl_cert_key] = key
165
+ end
166
+
167
+ # Sets the cert key password to use.
168
+ def ssl_cert_key_password(password)
169
+ @options[:ssl_cert_key_password] = password
170
+ end
171
+
172
+ # Sets the cert file to use.
173
+ def ssl_cert_file(file)
174
+ @options[:ssl_cert_file] = file
175
+ end
176
+
177
+ # Sets the cert to use.
178
+ def ssl_cert(cert)
179
+ @options[:ssl_cert] = cert
180
+ end
181
+
182
+ # Sets the CA cert file to use.
183
+ def ssl_ca_cert_file(file)
184
+ @options[:ssl_ca_cert_file] = file
185
+ end
186
+
187
+ # Sets the CA cert to use.
188
+ def ssl_ca_cert(cert)
189
+ @options[:ssl_ca_cert] = cert
190
+ end
191
+
192
+ # Sets the SSL ciphers to use.
193
+ def ssl_ciphers(ciphers)
194
+ @options[:ssl_ciphers] = ciphers
195
+ end
196
+
197
+ # Sets the CA cert path.
198
+ def ssl_ca_cert_path(path)
199
+ @options[:ssl_ca_cert_path] = path
200
+ end
201
+
202
+ # Sets the SSL cert store.
203
+ def ssl_cert_store(store)
204
+ @options[:ssl_cert_store] = store
205
+ end
206
+
207
+ # HTTP basic auth credentials.
208
+ def basic_auth(*credentials)
209
+ @options[:basic_auth] = credentials.flatten
210
+ end
211
+
212
+ # HTTP digest auth credentials.
213
+ def digest_auth(*credentials)
214
+ @options[:digest_auth] = credentials.flatten
215
+ end
216
+
217
+ # NTLM auth credentials.
218
+ def ntlm(*credentials)
219
+ @options[:ntlm] = credentials.flatten
220
+ end
221
+
222
+ # Instruct requests to follow HTTP redirects.
223
+ def follow_redirects(follow_redirects)
224
+ @options[:follow_redirects] = follow_redirects
225
+ end
226
+
227
+ # Instruct Savon which HTTPI adapter to use instead of the default.
228
+ def adapter(adapter)
229
+ @options[:adapter] = adapter
230
+ end
231
+
232
+ private
233
+
234
+ def default_transport_option?(option)
235
+ TRANSPORT_DEFAULTS.key?(option) && self[option] == TRANSPORT_DEFAULTS[option]
236
+ end
237
+
238
+ # Attempts to load faraday. Returns true if available, false on LoadError.
239
+ def faraday_loaded?
240
+ require "faraday"
241
+ true
242
+ rescue LoadError
243
+ false
244
+ end
245
+ end
246
+
247
+ # Client-level options applied to every request made by a Savon::Client instance.
248
+ # Covers service location, SOAP configuration, logging, response parsing,
249
+ # and transport selection. HTTPI-specific options (proxy, timeouts, SSL, auth)
250
+ # come from HTTPITransportOptions.
70
251
  class GlobalOptions < Options
71
252
  include SharedOptions
253
+ include HTTPITransportOptions
72
254
 
73
255
  def initialize(options = {})
74
256
  @option_type = :global
75
-
76
- defaults = {
77
- :encoding => "UTF-8",
78
- :soap_version => 1,
79
- :namespaces => {},
80
- :logger => Logger.new($stdout),
81
- :log => false,
82
- :log_headers => true,
83
- :filters => [],
84
- :pretty_print_xml => false,
85
- :raise_errors => true,
86
- :strip_namespaces => true,
87
- :delete_namespace_attributes => false,
88
- :convert_response_tags_to => lambda { |tag| StringUtils.snakecase(tag).to_sym},
89
- :convert_attributes_to => lambda { |k,v| [k,v] },
90
- :multipart => false,
91
- :adapter => nil,
92
- :use_wsa_headers => false,
93
- :no_message_tag => false,
94
- :follow_redirects => false,
95
- :unwrap => false,
96
- :host => nil
97
- }
98
-
99
257
  options = defaults.merge(options)
100
258
 
101
259
  # this option is a shortcut on the logger which needs to be set
@@ -112,7 +270,7 @@ module Savon
112
270
  @options[:wsdl] = wsdl_address
113
271
  end
114
272
 
115
- # set different host for actions in WSDL
273
+ # Set a different host for actions in the WSDL.
116
274
  def host(host)
117
275
  @options[:host] = host
118
276
  end
@@ -127,7 +285,7 @@ module Savon
127
285
  @options[:namespace] = namespace
128
286
  end
129
287
 
130
- # The namespace identifer.
288
+ # The namespace identifier.
131
289
  def namespace_identifier(identifier)
132
290
  @options[:namespace_identifier] = identifier
133
291
  end
@@ -137,31 +295,11 @@ module Savon
137
295
  @options[:namespaces] = namespaces
138
296
  end
139
297
 
140
- # Proxy server to use for all requests.
141
- def proxy(proxy)
142
- @options[:proxy] = proxy unless proxy.nil?
143
- end
144
-
145
- # A Hash of HTTP headers.
298
+ # A Hash of HTTP headers sent with every request.
146
299
  def headers(headers)
147
300
  @options[:headers] = headers
148
301
  end
149
302
 
150
- # Open timeout in seconds.
151
- def open_timeout(open_timeout)
152
- @options[:open_timeout] = open_timeout
153
- end
154
-
155
- # Read timeout in seconds.
156
- def read_timeout(read_timeout)
157
- @options[:read_timeout] = read_timeout
158
- end
159
-
160
- # Write timeout in seconds.
161
- def write_timeout(write_timeout)
162
- @options[:write_timeout] = write_timeout
163
- end
164
-
165
303
  # The encoding to use. Defaults to "UTF-8".
166
304
  def encoding(encoding)
167
305
  @options[:encoding] = encoding
@@ -210,7 +348,7 @@ module Savon
210
348
 
211
349
  # Changes the Logger's log level.
212
350
  def log_level(level)
213
- levels = { :debug => 0, :info => 1, :warn => 2, :error => 3, :fatal => 4 }
351
+ levels = { debug: 0, info: 1, warn: 2, error: 3, fatal: 4 }
214
352
 
215
353
  unless levels.include? level
216
354
  raise ArgumentError, "Invalid log level: #{level.inspect}\n" \
@@ -220,7 +358,7 @@ module Savon
220
358
  @options[:logger].level = levels[level]
221
359
  end
222
360
 
223
- # To log headers or not.
361
+ # Whether to log headers.
224
362
  def log_headers(log_headers)
225
363
  @options[:log_headers] = log_headers
226
364
  end
@@ -235,90 +373,6 @@ module Savon
235
373
  @options[:pretty_print_xml] = pretty_print_xml
236
374
  end
237
375
 
238
- # Specifies the SSL version to use.
239
- def ssl_version(version)
240
- @options[:ssl_version] = version
241
- end
242
-
243
- # Specifies the SSL version to use.
244
- def ssl_min_version(version)
245
- @options[:ssl_min_version] = version
246
- end
247
-
248
- # Specifies the SSL version to use.
249
- def ssl_max_version(version)
250
- @options[:ssl_max_version] = version
251
- end
252
-
253
- # Whether and how to to verify the connection.
254
- def ssl_verify_mode(verify_mode)
255
- @options[:ssl_verify_mode] = verify_mode
256
- end
257
-
258
- # Sets the cert key file to use.
259
- def ssl_cert_key_file(file)
260
- @options[:ssl_cert_key_file] = file
261
- end
262
-
263
- # Sets the cert key to use.
264
- def ssl_cert_key(key)
265
- @options[:ssl_cert_key] = key
266
- end
267
-
268
- # Sets the cert key password to use.
269
- def ssl_cert_key_password(password)
270
- @options[:ssl_cert_key_password] = password
271
- end
272
-
273
- # Sets the cert file to use.
274
- def ssl_cert_file(file)
275
- @options[:ssl_cert_file] = file
276
- end
277
-
278
- # Sets the cert to use.
279
- def ssl_cert(cert)
280
- @options[:ssl_cert] = cert
281
- end
282
-
283
- # Sets the ca cert file to use.
284
- def ssl_ca_cert_file(file)
285
- @options[:ssl_ca_cert_file] = file
286
- end
287
-
288
- # Sets the ca cert to use.
289
- def ssl_ca_cert(cert)
290
- @options[:ssl_ca_cert] = cert
291
- end
292
-
293
- def ssl_ciphers(ciphers)
294
- @options[:ssl_ciphers] = ciphers
295
- end
296
-
297
- # Sets the ca cert path.
298
- def ssl_ca_cert_path(path)
299
- @options[:ssl_ca_cert_path] = path
300
- end
301
-
302
- # Sets the ssl cert store.
303
- def ssl_cert_store(store)
304
- @options[:ssl_cert_store] = store
305
- end
306
-
307
- # HTTP basic auth credentials.
308
- def basic_auth(*credentials)
309
- @options[:basic_auth] = credentials.flatten
310
- end
311
-
312
- # HTTP digest auth credentials.
313
- def digest_auth(*credentials)
314
- @options[:digest_auth] = credentials.flatten
315
- end
316
-
317
- # NTLM auth credentials.
318
- def ntlm(*credentials)
319
- @options[:ntlm] = credentials.flatten
320
- end
321
-
322
376
  # Instruct Nori whether to strip namespaces from XML nodes.
323
377
  def strip_namespaces(strip_namespaces)
324
378
  @options[:strip_namespaces] = strip_namespaces
@@ -329,57 +383,113 @@ module Savon
329
383
  @options[:delete_namespace_attributes] = delete_namespace_attributes
330
384
  end
331
385
 
386
+ # The value Nori assigns to empty XML tags in the SOAP response.
387
+ # Defaults to nil, matching Nori's default; set to "" to map empty tags
388
+ # to an empty String instead.
389
+ def empty_tag_value(value)
390
+ @options[:empty_tag_value] = value
391
+ end
392
+
393
+ # Instruct Nori whether to convert dashes in response tag names to
394
+ # underscores before they become Hash keys. Defaults to true.
395
+ def convert_dashes_to_underscores(convert)
396
+ @options[:convert_dashes_to_underscores] = convert
397
+ end
398
+
399
+ # Instruct Nori whether to scrub invalid byte sequences from the response
400
+ # body before parsing it. Defaults to true, which lets responses containing
401
+ # invalid characters still be parsed.
402
+ def scrub_xml(scrub)
403
+ @options[:scrub_xml] = scrub
404
+ end
405
+
332
406
  # Tell Gyoku how to convert Hash key Symbols to XML tags.
333
407
  # Accepts one of :lower_camelcase, :camelcase, :upcase, or :none.
334
408
  def convert_request_keys_to(converter)
335
409
  @options[:convert_request_keys_to] = converter
336
410
  end
337
411
 
338
- # Tell Gyoku to unwrap Array of Hashes
339
- # Accepts a boolean, default to false
412
+ # Tell Gyoku to unwrap Array of Hashes.
413
+ # Accepts a boolean, defaults to false.
340
414
  def unwrap(unwrap)
341
415
  @options[:unwrap] = unwrap
342
416
  end
343
417
 
344
418
  # Tell Nori how to convert XML tags from the SOAP response into Hash keys.
345
419
  # Accepts a lambda or a block which receives an XML tag and returns a Hash key.
346
- # Defaults to convert tags to snakecase Symbols.
420
+ # Defaults to converting tags to snakecase Symbols.
347
421
  def convert_response_tags_to(converter = nil, &block)
348
422
  @options[:convert_response_tags_to] = block || converter
349
423
  end
350
424
 
351
425
  # Tell Nori how to convert XML attributes on tags from the SOAP response into Hash keys.
352
426
  # Accepts a lambda or a block which receives an XML tag and returns a Hash key.
353
- # Defaults to doing nothing
427
+ # Defaults to doing nothing.
354
428
  def convert_attributes_to(converter = nil, &block)
355
429
  @options[:convert_attributes_to] = block || converter
356
430
  end
357
431
 
358
432
  # Instruct Savon to create a multipart response if available.
359
433
  def multipart(multipart)
434
+ if multipart
435
+ warn "The global :multipart option has been a no-op since v2.13.0. " \
436
+ "Savon detects whether the response is multipart by checking if the " \
437
+ "response Content-Type header contains 'multipart'. You can remove" \
438
+ "this option from your code to make this warning disappear.", uplevel: 1
439
+ end
360
440
  @options[:multipart] = multipart
361
441
  end
362
442
 
363
- # Instruct Savon what HTTPI adapter it should use instead of default
364
- def adapter(adapter)
365
- @options[:adapter] = adapter
366
- end
367
-
368
443
  # Enable inclusion of WS-Addressing headers.
369
444
  def use_wsa_headers(use)
370
445
  @options[:use_wsa_headers] = use
371
446
  end
372
447
 
448
+ # Suppress the message tag wrapper around the SOAP body.
373
449
  def no_message_tag(bool)
374
450
  @options[:no_message_tag] = bool
375
451
  end
376
452
 
377
- # Instruct requests to follow HTTP redirects.
378
- def follow_redirects(follow_redirects)
379
- @options[:follow_redirects] = follow_redirects
453
+ # HTTP transport to use. Accepts :httpi (default) or :faraday.
454
+ # With :faraday, set proxy, timeout, TLS, auth, redirect, and adapter
455
+ # details on `client.faraday`; the HTTPI transport options are not copied.
456
+ def transport(transport)
457
+ @options[:transport] = transport
458
+ end
459
+
460
+ private
461
+
462
+ # The default value for every global option.
463
+ def defaults
464
+ HTTPITransportOptions::TRANSPORT_DEFAULTS.merge(
465
+ encoding: "UTF-8",
466
+ soap_version: 1,
467
+ namespaces: {},
468
+ logger: Logger.new($stdout),
469
+ log: false,
470
+ log_headers: true,
471
+ filters: [],
472
+ pretty_print_xml: false,
473
+ raise_errors: true,
474
+ strip_namespaces: true,
475
+ delete_namespace_attributes: false,
476
+ empty_tag_value: nil,
477
+ convert_dashes_to_underscores: true,
478
+ scrub_xml: true,
479
+ convert_response_tags_to: ->(tag) { StringUtils.snakecase(tag).to_sym },
480
+ convert_attributes_to: ->(k, v) { [k, v] },
481
+ multipart: false,
482
+ use_wsa_headers: false,
483
+ no_message_tag: false,
484
+ unwrap: false,
485
+ host: nil,
486
+ transport: :httpi
487
+ )
380
488
  end
381
489
  end
382
490
 
491
+ # Per-request options passed to client.call.
492
+ # Overrides or extends the matching GlobalOptions for a single SOAP operation.
383
493
  class LocalOptions < Options
384
494
  include SharedOptions
385
495
 
@@ -387,9 +497,9 @@ module Savon
387
497
  @option_type = :local
388
498
 
389
499
  defaults = {
390
- :advanced_typecasting => true,
391
- :response_parser => :nokogiri,
392
- :multipart => false
500
+ advanced_typecasting: true,
501
+ response_parser: :nokogiri,
502
+ multipart: false
393
503
  }
394
504
 
395
505
  super defaults.merge(options)
@@ -397,7 +507,7 @@ module Savon
397
507
 
398
508
  # The local SOAP header. Expected to be a Hash or respond to #to_s.
399
509
  # Will be merged with the global SOAP header if both are Hashes.
400
- # Otherwise the local option will be prefered.
510
+ # Otherwise the local option will be preferred.
401
511
  def soap_header(header)
402
512
  @options[:soap_header] = header
403
513
  end
@@ -479,9 +589,16 @@ module Savon
479
589
 
480
590
  # Instruct Savon to create a multipart response if available.
481
591
  def multipart(multipart)
592
+ if multipart
593
+ warn "The local :multipart option has been a no-op since v2.13.0. " \
594
+ "Savon detects whether the response is multipart by checking if the " \
595
+ "response Content-Type header contains 'multipart'. You can remove" \
596
+ "this option from your code to make this warning disappear.", uplevel: 1
597
+ end
482
598
  @options[:multipart] = multipart
483
599
  end
484
600
 
601
+ # Per-request HTTP headers. Merged with global headers for each request.
485
602
  def headers(headers)
486
603
  @options[:headers] = headers
487
604
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "gyoku"
3
4
 
4
5
  module Savon
@@ -37,7 +38,7 @@ module Savon
37
38
  private
38
39
 
39
40
  def translate_tag(key)
40
- Gyoku.xml_tag(key, :key_converter => @key_converter).to_s
41
+ Gyoku.xml_tag(key, key_converter: @key_converter).to_s
41
42
  end
42
43
 
43
44
  def add_namespaces_to_values(values, path)