savon 2.12.1 → 2.13.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +91 -73
  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 +3 -2
  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 +1 -0
  27. data/lib/savon/version.rb +2 -1
  28. data/lib/savon.rb +1 -0
  29. data/savon.gemspec +9 -8
  30. data/spec/integration/support/application.rb +33 -1
  31. data/spec/integration/support/server.rb +1 -0
  32. data/spec/integration/zipcode_example_spec.rb +5 -8
  33. data/spec/savon/builder_spec.rb +2 -1
  34. data/spec/savon/client_spec.rb +5 -4
  35. data/spec/savon/core_ext/string_spec.rb +2 -1
  36. data/spec/savon/features/message_tag_spec.rb +2 -1
  37. data/spec/savon/http_error_spec.rb +9 -1
  38. data/spec/savon/log_message_spec.rb +2 -1
  39. data/spec/savon/message_spec.rb +2 -11
  40. data/spec/savon/mock_spec.rb +2 -1
  41. data/spec/savon/model_spec.rb +2 -1
  42. data/spec/savon/multipart_request_spec.rb +46 -0
  43. data/spec/savon/observers_spec.rb +2 -1
  44. data/spec/savon/operation_spec.rb +20 -43
  45. data/spec/savon/options_spec.rb +40 -1
  46. data/spec/savon/qualified_message_spec.rb +2 -1
  47. data/spec/savon/request_logger_spec.rb +2 -1
  48. data/spec/savon/request_spec.rb +47 -6
  49. data/spec/savon/response_spec.rb +2 -1
  50. data/spec/savon/soap_fault_spec.rb +2 -1
  51. data/spec/savon/softlayer_spec.rb +17 -2
  52. data/spec/spec_helper.rb +5 -4
  53. data/spec/support/adapters.rb +1 -0
  54. data/spec/support/endpoint.rb +1 -0
  55. data/spec/support/fixture.rb +1 -0
  56. data/spec/support/integration.rb +1 -0
  57. data/spec/support/stdout.rb +1 -0
  58. metadata +55 -33
  59. data/.travis.yml +0 -26
  60. data/donate.png +0 -0
  61. data/spec/integration/centra_spec.rb +0 -67
  62. data/spec/integration/email_example_spec.rb +0 -32
  63. data/spec/integration/random_quote_spec.rb +0 -23
  64. data/spec/integration/ratp_example_spec.rb +0 -28
  65. data/spec/integration/stockquote_example_spec.rb +0 -34
  66. data/spec/integration/temperature_example_spec.rb +0 -46
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
@@ -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)