savon 2.12.0 → 2.13.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 (68) hide show
  1. checksums.yaml +5 -5
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +98 -76
  4. data/CONTRIBUTING.md +15 -19
  5. data/Gemfile +2 -7
  6. data/README.md +15 -19
  7. data/RELEASING.md +10 -0
  8. data/lib/savon/block_interface.rb +1 -0
  9. data/lib/savon/builder.rb +98 -29
  10. data/lib/savon/client.rb +1 -0
  11. data/lib/savon/core_ext/string.rb +1 -0
  12. data/lib/savon/header.rb +2 -6
  13. data/lib/savon/http_error.rb +4 -4
  14. data/lib/savon/log_message.rb +1 -0
  15. data/lib/savon/message.rb +1 -0
  16. data/lib/savon/mock/expectation.rb +1 -0
  17. data/lib/savon/mock/spec_helper.rb +1 -0
  18. data/lib/savon/mock.rb +1 -0
  19. data/lib/savon/model.rb +1 -0
  20. data/lib/savon/operation.rb +20 -18
  21. data/lib/savon/options.rb +56 -0
  22. data/lib/savon/qualified_message.rb +4 -3
  23. data/lib/savon/request.rb +5 -0
  24. data/lib/savon/request_logger.rb +8 -2
  25. data/lib/savon/response.rb +48 -1
  26. data/lib/savon/soap_fault.rb +2 -1
  27. data/lib/savon/version.rb +2 -1
  28. data/lib/savon.rb +1 -0
  29. data/savon.gemspec +9 -8
  30. data/spec/fixtures/response/empty_soap_fault.xml +13 -0
  31. data/spec/fixtures/wsdl/elements_in_types.xml +43 -0
  32. data/spec/integration/support/application.rb +33 -1
  33. data/spec/integration/support/server.rb +1 -0
  34. data/spec/integration/zipcode_example_spec.rb +5 -8
  35. data/spec/savon/builder_spec.rb +2 -1
  36. data/spec/savon/client_spec.rb +5 -4
  37. data/spec/savon/core_ext/string_spec.rb +2 -1
  38. data/spec/savon/features/message_tag_spec.rb +2 -1
  39. data/spec/savon/http_error_spec.rb +9 -1
  40. data/spec/savon/log_message_spec.rb +2 -1
  41. data/spec/savon/message_spec.rb +2 -11
  42. data/spec/savon/mock_spec.rb +2 -1
  43. data/spec/savon/model_spec.rb +2 -1
  44. data/spec/savon/multipart_request_spec.rb +46 -0
  45. data/spec/savon/observers_spec.rb +2 -1
  46. data/spec/savon/operation_spec.rb +20 -43
  47. data/spec/savon/options_spec.rb +51 -1
  48. data/spec/savon/qualified_message_spec.rb +2 -1
  49. data/spec/savon/request_logger_spec.rb +2 -1
  50. data/spec/savon/request_spec.rb +47 -6
  51. data/spec/savon/response_spec.rb +2 -1
  52. data/spec/savon/soap_fault_spec.rb +12 -1
  53. data/spec/savon/softlayer_spec.rb +17 -2
  54. data/spec/spec_helper.rb +5 -4
  55. data/spec/support/adapters.rb +1 -0
  56. data/spec/support/endpoint.rb +1 -0
  57. data/spec/support/fixture.rb +1 -0
  58. data/spec/support/integration.rb +1 -0
  59. data/spec/support/stdout.rb +1 -0
  60. metadata +57 -34
  61. data/.travis.yml +0 -18
  62. data/donate.png +0 -0
  63. data/spec/integration/centra_spec.rb +0 -67
  64. data/spec/integration/email_example_spec.rb +0 -32
  65. data/spec/integration/random_quote_spec.rb +0 -23
  66. data/spec/integration/ratp_example_spec.rb +0 -28
  67. data/spec/integration/stockquote_example_spec.rb +0 -34
  68. data/spec/integration/temperature_example_spec.rb +0 -46
data/Gemfile CHANGED
@@ -1,13 +1,8 @@
1
1
  source 'https://rubygems.org'
2
+
2
3
  gemspec
3
4
 
4
5
  gem "httpclient", "~> 2.7.1"
5
6
 
6
7
  gem "simplecov", :require => false
7
- gem "coveralls", :require => false
8
-
9
- platform :rbx do
10
- gem 'racc'
11
- gem 'rubysl'
12
- gem 'rubinius-coverage'
13
- end
8
+ gem "net-smtp" if RUBY_VERSION >= "3.1.0"
data/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  Heavy metal SOAP client
4
4
 
5
- [Documentation](http://savonrb.com) | [RDoc](http://rubydoc.info/gems/savon) |
5
+ [Documentation](https://www.rubydoc.info/gems/savon/) | [Support](https://stackoverflow.com/questions/tagged/savon) |
6
6
  [Mailing list](https://groups.google.com/forum/#!forum/savonrb) | [Twitter](http://twitter.com/savonrb)
7
7
 
8
- [![Build Status](https://secure.travis-ci.org/savonrb/savon.svg?branch=master)](http://travis-ci.org/savonrb/savon)
8
+ [![Ruby](https://github.com/savonrb/savon/actions/workflows/ci.yml/badge.svg)](https://github.com/savonrb/savon/actions/workflows/ci.yml)
9
9
  [![Gem Version](https://badge.fury.io/rb/savon.svg)](http://badge.fury.io/rb/savon)
10
10
  [![Code Climate](https://codeclimate.com/github/savonrb/savon.svg)](https://codeclimate.com/github/savonrb/savon)
11
- [![Coverage Status](https://coveralls.io/repos/savonrb/savon/badge.svg?branch=version2)](https://coveralls.io/r/savonrb/savon)
11
+ [![Coverage Status](https://coveralls.io/repos/savonrb/savon/badge.svg)](https://coveralls.io/r/savonrb/savon)
12
12
 
13
13
 
14
14
  ## Version 2
@@ -22,7 +22,7 @@ $ gem install savon
22
22
  or add it to your Gemfile like this:
23
23
 
24
24
  ```
25
- gem 'savon', '~> 2.12.0'
25
+ gem 'savon', '~> 2.13.0'
26
26
  ```
27
27
 
28
28
  ## Usage example
@@ -33,6 +33,12 @@ require 'savon'
33
33
  # create a client for the service
34
34
  client = Savon.client(wsdl: 'http://service.example.com?wsdl')
35
35
 
36
+ # or: create a client with a wsdl provided as a string
37
+ client = Savon.client do |config|
38
+ wsdl_content = File.read("/path/to/wsdl")
39
+ config.wsdl wsdl_content
40
+ end
41
+
36
42
  client.operations
37
43
  # => [:find_user, :list_users]
38
44
 
@@ -47,10 +53,12 @@ For more examples, you should check out the
47
53
  [integration tests](https://github.com/savonrb/savon/tree/version2/spec/integration).
48
54
 
49
55
  ## Ruby version support
50
- * 2.12.x - MRI 2.2, 2.3, 2.4
56
+
57
+ * `master` - MRI 2.7, 3.0, 3.1 (same support as Ruby)
58
+ * 2.12.x - MRI 2.2, 2.3, 2.4, 2.5
51
59
  * 2.11.x - MRI 2.0, 2.1, 2.2, and 2.3
52
60
 
53
- If you are running MRI 1.8.7, try the 2.6.x branch.
61
+ If you are running MRI 1.8.7, try a 2.6.x release.
54
62
 
55
63
  ## Running tests
56
64
 
@@ -64,19 +72,7 @@ $ bundle exec rspec
64
72
  * URI::InvalidURIError -- if you see this error, then it is likely that the http client you are using cannot parse the URI for your WSDL. Try `gem install httpclient` or add it to your `Gemfile`.
65
73
  - See https://github.com/savonrb/savon/issues/488 for more info
66
74
 
67
- ## Give back
68
-
69
- If you're using Savon and you or your company is making money from it, then please consider
70
- donating via [Gittip](https://www.gittip.com/tjarratt/) so that I can continue to improve it.
71
-
72
- [![donate](donate.png)](https://www.gittip.com/tjarratt/)
73
-
74
75
 
75
76
  ## Documentation
76
77
 
77
- Please make sure to [read the documentation](http://savonrb.com/version2/).
78
-
79
- And if you find any problems with it or if you think something's missing,
80
- feel free to [help out and improve the documentation](https://github.com/savonrb/savonrb.com).
81
-
82
- Donate icon from the [Noun Project](http://thenounproject.com/noun/donate/#icon-No285).
78
+ Please be sure to [read the documentation](https://www.rubydoc.info/github/savonrb/savon/).
data/RELEASING.md ADDED
@@ -0,0 +1,10 @@
1
+ _You'll need write access to the repository and the [rubygems](https://rubygems.org/gems/savon) project to create a release._
2
+
3
+ 1. On master, edit [CHANGELOG.md](https://github.com/savonrb/savon/blob/master/CHANGELOG.md) to finalize the new version number and list of all changes.
4
+ 2. Bump [version.rb](https://github.com/savonrb/savon/blob/master/lib/savon/version.rb) to the version you picked in previous step.
5
+ 3. **Final check**: make sure all tests are green, and that `gem build savon.gemspec` on master succeeds. If not, merge any fixes back to master and go to step 1.
6
+ 4. [Draft a new release](https://github.com/savonrb/savon/releases/new) on Github.
7
+ - Create a tag matching the version in previous step - e.g. `v2.12.1` - prepend the version number with a "v".
8
+ - Use `v[version]` for the release title, and copy the changelog into the release notes.
9
+ - Click "Publish release" to commit the tag on Github.
10
+ 5. `git checkout` the newly commited tag, then `gem build savon.gemspec` to build the gem package locally. Use `gem push savon-[version].gem` to publish to rubygems.
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Savon
2
3
  class BlockInterface
3
4
 
data/lib/savon/builder.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "savon/header"
2
3
  require "savon/message"
3
4
  require "nokogiri"
@@ -6,6 +7,7 @@ require "gyoku"
6
7
 
7
8
  module Savon
8
9
  class Builder
10
+ attr_reader :multipart
9
11
 
10
12
  SCHEMA_TYPES = {
11
13
  "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema",
@@ -36,14 +38,7 @@ module Savon
36
38
  end
37
39
 
38
40
  def build_document
39
- xml_result = tag(builder, :Envelope, namespaces_with_globals) do |xml|
40
- tag(xml, :Header, header_attributes) { xml << header.to_s } unless header.empty?
41
- if @globals[:no_message_tag]
42
- tag(xml, :Body, body_attributes) { xml << message.to_s }
43
- else
44
- tag(xml, :Body, body_attributes) { xml.tag!(*namespaced_message_tag) { xml << body_message } }
45
- end
46
- end
41
+ xml_result = build_xml
47
42
 
48
43
  # if we have a signature sign the document
49
44
  if @signature
@@ -51,20 +46,19 @@ module Savon
51
46
 
52
47
  2.times do
53
48
  @header = nil
54
- @signature.document = tag(builder, :Envelope, namespaces_with_globals) do |xml|
55
- tag(xml, :Header, header_attributes) { xml << header.to_s } unless header.empty?
56
- if @globals[:no_message_tag]
57
- tag(xml, :Body, body_attributes) { xml << message.to_s }
58
- else
59
- tag(xml, :Body, body_attributes) { xml.tag!(*namespaced_message_tag) { xml << message.to_s } }
60
- end
61
- end
49
+ @signature.document = build_xml
62
50
  end
63
51
 
64
52
  xml_result = @signature.document
65
53
  end
66
54
 
67
- xml_result
55
+ # if there are attachments for the request, we should build a multipart message according to
56
+ # https://www.w3.org/TR/SOAP-attachments
57
+ if @locals[:attachments]
58
+ build_multipart_message(xml_result)
59
+ else
60
+ xml_result
61
+ end
68
62
  end
69
63
 
70
64
  def header_attributes
@@ -117,15 +111,24 @@ module Savon
117
111
  @namespaces ||= begin
118
112
  namespaces = SCHEMA_TYPES.dup
119
113
 
120
- if namespace_identifier == nil
121
- namespaces["xmlns"] = @globals[:namespace] || @wsdl.namespace
122
- else
123
- namespaces["xmlns:#{namespace_identifier}"] = @globals[:namespace] || @wsdl.namespace
124
- end
114
+ # check namespace_identifier
115
+ namespaces["xmlns#{namespace_identifier.nil? ? '' : ":#{namespace_identifier}"}"] =
116
+ @globals[:namespace] || @wsdl.namespace
117
+
118
+ # check env_namespace
119
+ namespaces["xmlns#{env_namespace && env_namespace != "" ? ":#{env_namespace}" : ''}"] =
120
+ SOAP_NAMESPACE[@globals[:soap_version]]
121
+
122
+ if @wsdl&.document
123
+ @wsdl.parser.namespaces.each do |identifier, path|
124
+ prefixed_identifier = identifier
125
+ prefixed_identifier = "xmlns:#{prefixed_identifier}" unless prefixed_identifier == 'xmlns'
125
126
 
126
- key = ["xmlns"]
127
- key << env_namespace if env_namespace && env_namespace != ""
128
- namespaces[key.join(":")] = SOAP_NAMESPACE[@globals[:soap_version]]
127
+ next if namespaces.key?(prefixed_identifier)
128
+
129
+ namespaces[prefixed_identifier] = path
130
+ end
131
+ end
129
132
 
130
133
  namespaces
131
134
  end
@@ -162,18 +165,20 @@ module Savon
162
165
  break if @locals[:message].nil?
163
166
  message_locals = @locals[:message][message.snakecase.to_sym]
164
167
  message_content = Message.new(message_tag, namespace_identifier, @types, @used_namespaces, message_locals, :unqualified, @globals[:convert_request_keys_to], @globals[:unwrap]).to_s
165
- messages << "<#{message} xsi:type=\"#{type.join(':')}\">#{message_content}</#{message}>"
168
+ messages += "<#{message} xsi:type=\"#{type.join(':')}\">#{message_content}</#{message}>"
166
169
  end
167
170
  messages
168
171
  end
169
172
 
170
173
  def message_tag
171
- message_tag = @wsdl.soap_input(@operation_name.to_sym).keys.first if @wsdl.document? and @wsdl.soap_input(@operation_name.to_sym).is_a?(Hash)
174
+ wsdl_tag_name = @wsdl.document? && @wsdl.soap_input(@operation_name.to_sym)
175
+
176
+ message_tag = wsdl_tag_name.keys.first if wsdl_tag_name.is_a?(Hash)
172
177
  message_tag ||= @locals[:message_tag]
173
- message_tag ||= @wsdl.soap_input(@operation_name.to_sym) if @wsdl.document?
178
+ message_tag ||= wsdl_tag_name
174
179
  message_tag ||= Gyoku.xml_tag(@operation_name, :key_converter => @globals[:convert_request_keys_to])
175
180
 
176
- @message_tag = message_tag.to_sym
181
+ message_tag.to_sym
177
182
  end
178
183
 
179
184
  def message_attributes
@@ -227,5 +232,69 @@ module Savon
227
232
  end
228
233
  end
229
234
 
235
+ def build_xml
236
+ tag(builder, :Envelope, namespaces_with_globals) do |xml|
237
+ tag(xml, :Header, header_attributes) { xml << header.to_s } unless header.empty?
238
+ tag(xml, :Body, body_attributes) do
239
+ if @globals[:no_message_tag]
240
+ xml << message.to_s
241
+ else
242
+ xml.tag!(*namespaced_message_tag) { xml << body_message }
243
+ end
244
+ end
245
+ end
246
+ end
247
+
248
+ def build_multipart_message(message_xml)
249
+ multipart_message = init_multipart_message(message_xml)
250
+ add_attachments_to_multipart_message(multipart_message)
251
+
252
+ multipart_message.ready_to_send!
253
+
254
+ # the mail.body.encoded algorithm reorders the parts, default order is [ "text/plain", "text/enriched", "text/html" ]
255
+ # should redefine the sort order, because the soap request xml should be the first
256
+ multipart_message.body.set_sort_order [ "text/xml" ]
257
+
258
+ multipart_message.body.encoded(multipart_message.content_transfer_encoding)
259
+ end
260
+
261
+ def init_multipart_message(message_xml)
262
+ multipart_message = Mail.new
263
+ xml_part = Mail::Part.new do
264
+ content_type 'text/xml'
265
+ body message_xml
266
+ # in Content-Type the start parameter is recommended (RFC 2387)
267
+ content_id '<soap-request-body@soap>'
268
+ end
269
+ multipart_message.add_part xml_part
270
+
271
+ #request.headers["Content-Type"] = "multipart/related; boundary=\"#{multipart_message.body.boundary}\"; type=\"text/xml\"; start=\"#{xml_part.content_id}\""
272
+ @multipart = {
273
+ multipart_boundary: multipart_message.body.boundary,
274
+ start: xml_part.content_id,
275
+ }
276
+
277
+ multipart_message
278
+ end
279
+
280
+ def add_attachments_to_multipart_message(multipart_message)
281
+ if @locals[:attachments].is_a? Hash
282
+ # hash example: { 'att1' => '/path/to/att1', 'att2' => '/path/to/att2' }
283
+ @locals[:attachments].each do |identifier, attachment|
284
+ add_attachment_to_multipart_message(multipart_message, attachment, identifier)
285
+ end
286
+ elsif @locals[:attachments].is_a? Array
287
+ # array example: [ '/path/to/att1', '/path/to/att2' ]
288
+ # array example: [ { filename: 'att1.xml', content: '<x/>' }, { filename: 'att2.xml', content: '<y/>' } ]
289
+ @locals[:attachments].each do |attachment|
290
+ add_attachment_to_multipart_message(multipart_message, attachment, attachment.is_a?(String) ? File.basename(attachment) : attachment[:filename])
291
+ end
292
+ end
293
+ end
294
+
295
+ def add_attachment_to_multipart_message(multipart_message, attachment, identifier)
296
+ multipart_message.add_file attachment.clone
297
+ multipart_message.parts.last.content_id = multipart_message.parts.last.content_location = identifier.to_s
298
+ end
230
299
  end
231
300
  end
data/lib/savon/client.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "savon/operation"
2
3
  require "savon/request"
3
4
  require "savon/options"
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
 
2
3
  module Savon
3
4
  module CoreExt
data/lib/savon/header.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "akami"
2
3
  require "gyoku"
3
4
  require "securerandom"
@@ -61,12 +62,7 @@ module Savon
61
62
  convert_to_xml({
62
63
  'wsa:Action' => @locals[:soap_action],
63
64
  '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
- }
65
+ 'wsa:MessageID' => "urn:uuid:#{SecureRandom.uuid}"
70
66
  })
71
67
  end
72
68
 
@@ -1,4 +1,4 @@
1
- require "savon"
1
+ # frozen_string_literal: true
2
2
 
3
3
  module Savon
4
4
  class HTTPError < Error
@@ -14,9 +14,9 @@ module Savon
14
14
  attr_reader :http
15
15
 
16
16
  def to_s
17
- message = "HTTP error (#{@http.code})"
18
- message << ": #{@http.body}" unless @http.body.empty?
19
- message
17
+ String.new("HTTP error (#{@http.code})").tap do |str_error|
18
+ str_error << ": #{@http.body}" unless @http.body.empty?
19
+ end
20
20
  end
21
21
 
22
22
  def to_hash
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "nokogiri"
2
3
 
3
4
  module Savon
data/lib/savon/message.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "savon/qualified_message"
2
3
  require "gyoku"
3
4
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "httpi"
2
3
 
3
4
  module Savon
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "savon/mock"
2
3
 
3
4
  module Savon
data/lib/savon/mock.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Savon
2
3
  class ExpectationError < StandardError; end
3
4
  end
data/lib/savon/model.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Savon
2
3
  module Model
3
4
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "savon/options"
2
3
  require "savon/block_interface"
3
4
  require "savon/request"
@@ -5,10 +6,16 @@ require "savon/builder"
5
6
  require "savon/response"
6
7
  require "savon/request_logger"
7
8
  require "savon/http_error"
9
+ require "mail"
8
10
 
9
11
  module Savon
10
12
  class Operation
11
13
 
14
+ SOAP_REQUEST_TYPE = {
15
+ 1 => "text/xml",
16
+ 2 => "application/soap+xml"
17
+ }
18
+
12
19
  def self.create(operation_name, wsdl, globals)
13
20
  if wsdl.document?
14
21
  ensure_name_is_symbol! operation_name
@@ -66,21 +73,7 @@ module Savon
66
73
  private
67
74
 
68
75
  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
76
+ Response.new(response, @globals, @locals)
84
77
  end
85
78
 
86
79
  def set_locals(locals, block)
@@ -107,6 +100,15 @@ module Savon
107
100
  request.url = endpoint
108
101
  request.body = builder.to_s
109
102
 
103
+ if builder.multipart
104
+ request.gzip
105
+ request.headers["Content-Type"] = ["multipart/related",
106
+ "type=\"#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}\"",
107
+ "start=\"#{builder.multipart[:start]}\"",
108
+ "boundary=\"#{builder.multipart[:multipart_boundary]}\""].join("; ")
109
+ request.headers["MIME-Version"] = "1.0"
110
+ end
111
+
110
112
  # TODO: could HTTPI do this automatically in case the header
111
113
  # was not specified manually? [dh, 2013-01-04]
112
114
  request.headers["Content-Length"] = request.body.bytesize.to_s
@@ -119,11 +121,11 @@ module Savon
119
121
  return if @locals.include?(:soap_action) && !@locals[:soap_action]
120
122
 
121
123
  # get the soap_action from local options
122
- soap_action = @locals[:soap_action]
124
+ @locals[:soap_action] ||
123
125
  # with no local option, but a wsdl, ask it for the soap_action
124
- soap_action ||= @wsdl.soap_action(@name.to_sym) if @wsdl.document?
126
+ @wsdl.document? && @wsdl.soap_action(@name.to_sym) ||
125
127
  # if there is no soap_action up to this point, fallback to a simple default
126
- soap_action ||= Gyoku.xml_tag(@name, :key_converter => @globals[:convert_request_keys_to])
128
+ Gyoku.xml_tag(@name, :key_converter => @globals[:convert_request_keys_to])
127
129
  end
128
130
 
129
131
  def endpoint
data/lib/savon/options.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "logger"
2
3
  require "httpi"
3
4
 
@@ -78,6 +79,7 @@ module Savon
78
79
  :namespaces => {},
79
80
  :logger => Logger.new($stdout),
80
81
  :log => false,
82
+ :log_headers => true,
81
83
  :filters => [],
82
84
  :pretty_print_xml => false,
83
85
  :raise_errors => true,
@@ -155,6 +157,11 @@ module Savon
155
157
  @options[:read_timeout] = read_timeout
156
158
  end
157
159
 
160
+ # Write timeout in seconds.
161
+ def write_timeout(write_timeout)
162
+ @options[:write_timeout] = write_timeout
163
+ end
164
+
158
165
  # The encoding to use. Defaults to "UTF-8".
159
166
  def encoding(encoding)
160
167
  @options[:encoding] = encoding
@@ -213,6 +220,11 @@ module Savon
213
220
  @options[:logger].level = levels[level]
214
221
  end
215
222
 
223
+ # To log headers or not.
224
+ def log_headers(log_headers)
225
+ @options[:log_headers] = log_headers
226
+ end
227
+
216
228
  # A list of XML tags to filter from logged SOAP messages.
217
229
  def filters(*filters)
218
230
  @options[:filters] = filters.flatten
@@ -228,6 +240,16 @@ module Savon
228
240
  @options[:ssl_version] = version
229
241
  end
230
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
+
231
253
  # Whether and how to to verify the connection.
232
254
  def ssl_verify_mode(verify_mode)
233
255
  @options[:ssl_verify_mode] = verify_mode
@@ -396,6 +418,40 @@ module Savon
396
418
  @options[:attributes] = attributes
397
419
  end
398
420
 
421
+ # Attachments for the SOAP message (https://www.w3.org/TR/SOAP-attachments)
422
+ #
423
+ # should pass an Array or a Hash; items should be path strings or
424
+ # { filename: 'file.name', content: 'content' } objects
425
+ # The Content-ID in multipart message sections will be the filename or the key if Hash is given
426
+ #
427
+ # usage examples:
428
+ #
429
+ # response = client.call :operation1 do
430
+ # message param1: 'value'
431
+ # attachments [
432
+ # { filename: 'x1.xml', content: '<xml>abc</xml>'},
433
+ # { filename: 'x2.xml', content: '<xml>abc</xml>'}
434
+ # ]
435
+ # end
436
+ # # Content-ID will be x1.xml and x2.xml
437
+ #
438
+ # response = client.call :operation1 do
439
+ # message param1: 'value'
440
+ # attachments 'x1.xml' => '/tmp/1281ab7d7d.xml', 'x2.xml' => '/tmp/4c5v8e833a.xml'
441
+ # end
442
+ # # Content-ID will be x1.xml and x2.xml
443
+ #
444
+ # response = client.call :operation1 do
445
+ # message param1: 'value'
446
+ # attachments [ '/tmp/1281ab7d7d.xml', '/tmp/4c5v8e833a.xml']
447
+ # end
448
+ # # Content-ID will be 1281ab7d7d.xml and 4c5v8e833a.xml
449
+ #
450
+ # The Content-ID is important if you want to refer to the attachments from the SOAP request
451
+ def attachments(attachments)
452
+ @options[:attachments] = attachments
453
+ end
454
+
399
455
  # Value of the SOAPAction HTTP header.
400
456
  def soap_action(soap_action)
401
457
  @options[:soap_action] = soap_action
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "gyoku"
2
3
 
3
4
  module Savon
@@ -26,7 +27,7 @@ module Savon
26
27
  translated_key = translate_tag(key)
27
28
  newkey = add_namespaces_to_values(key, path).first
28
29
  newpath = path + [translated_key]
29
- newhash[newkey] = to_hash(value, newpath)
30
+ newhash[newkey] = to_hash(value, @types[newpath] ? [@types[newpath]] : newpath)
30
31
  end
31
32
  end
32
33
  newhash
@@ -43,8 +44,8 @@ module Savon
43
44
  Array(values).collect do |value|
44
45
  translated_value = translate_tag(value)
45
46
  namespace_path = path + [translated_value]
46
- namespace = @used_namespaces[namespace_path]
47
- namespace.blank? ? value : "#{namespace}:#{translated_value}"
47
+ namespace = @used_namespaces[namespace_path] || ''
48
+ namespace.empty? ? value : "#{namespace}:#{translated_value}"
48
49
  end
49
50
  end
50
51
  end
data/lib/savon/request.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "httpi"
2
3
 
3
4
  module Savon
@@ -21,10 +22,14 @@ module Savon
21
22
  def configure_timeouts
22
23
  @http_request.open_timeout = @globals[:open_timeout] if @globals.include? :open_timeout
23
24
  @http_request.read_timeout = @globals[:read_timeout] if @globals.include? :read_timeout
25
+ @http_request.write_timeout = @globals[:write_timeout] if @globals.include? :write_timeout
24
26
  end
25
27
 
26
28
  def configure_ssl
27
29
  @http_request.auth.ssl.ssl_version = @globals[:ssl_version] if @globals.include? :ssl_version
30
+ @http_request.auth.ssl.min_version = @globals[:ssl_min_version] if @globals.include? :ssl_min_version
31
+ @http_request.auth.ssl.max_version = @globals[:ssl_max_version] if @globals.include? :ssl_max_version
32
+
28
33
  @http_request.auth.ssl.verify_mode = @globals[:ssl_verify_mode] if @globals.include? :ssl_verify_mode
29
34
  @http_request.auth.ssl.ciphers = @globals[:ssl_ciphers] if @globals.include? :ssl_ciphers
30
35
 
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  require "savon/log_message"
2
3
 
3
4
  module Savon
@@ -23,21 +24,26 @@ module Savon
23
24
  @globals[:log]
24
25
  end
25
26
 
27
+ def log_headers?
28
+ @globals[:log_headers]
29
+ end
30
+
26
31
  private
27
32
 
28
33
  def log_request(request)
29
34
  logger.info { "SOAP request: #{request.url}" }
30
- logger.info { headers_to_log(request.headers) }
35
+ logger.info { headers_to_log(request.headers) } if log_headers?
31
36
  logger.debug { body_to_log(request.body) }
32
37
  end
33
38
 
34
39
  def log_response(response)
35
40
  logger.info { "SOAP response (status #{response.code})" }
41
+ logger.debug { headers_to_log(response.headers) } if log_headers?
36
42
  logger.debug { body_to_log(response.body) }
37
43
  end
38
44
 
39
45
  def headers_to_log(headers)
40
- headers.map { |key, value| "#{key}: #{value}" }.join(", ")
46
+ headers.map { |key, value| "#{key}: #{value}" }.join("\n")
41
47
  end
42
48
 
43
49
  def body_to_log(body)