savon 2.12.0 → 2.15.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (95) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +115 -77
  3. data/README.md +23 -21
  4. data/lib/savon/block_interface.rb +1 -0
  5. data/lib/savon/builder.rb +100 -30
  6. data/lib/savon/client.rb +1 -0
  7. data/lib/savon/header.rb +2 -6
  8. data/lib/savon/http_error.rb +4 -4
  9. data/lib/savon/log_message.rb +1 -0
  10. data/lib/savon/message.rb +1 -0
  11. data/lib/savon/mock/expectation.rb +1 -0
  12. data/lib/savon/mock/spec_helper.rb +1 -0
  13. data/lib/savon/mock.rb +1 -0
  14. data/lib/savon/model.rb +4 -3
  15. data/lib/savon/operation.rb +20 -18
  16. data/lib/savon/options.rb +57 -1
  17. data/lib/savon/qualified_message.rb +4 -3
  18. data/lib/savon/request.rb +5 -0
  19. data/lib/savon/request_logger.rb +8 -2
  20. data/lib/savon/response.rb +51 -4
  21. data/lib/savon/soap_fault.rb +2 -1
  22. data/lib/savon/string_utils.rb +17 -0
  23. data/lib/savon/version.rb +2 -1
  24. data/lib/savon.rb +2 -0
  25. metadata +78 -101
  26. data/.gitignore +0 -14
  27. data/.travis.yml +0 -18
  28. data/.yardopts +0 -6
  29. data/CONTRIBUTING.md +0 -46
  30. data/Gemfile +0 -13
  31. data/donate.png +0 -0
  32. data/lib/savon/core_ext/string.rb +0 -29
  33. data/savon.gemspec +0 -46
  34. data/spec/fixtures/gzip/message.gz +0 -0
  35. data/spec/fixtures/response/another_soap_fault.xml +0 -14
  36. data/spec/fixtures/response/authentication.xml +0 -14
  37. data/spec/fixtures/response/f5.xml +0 -39
  38. data/spec/fixtures/response/header.xml +0 -13
  39. data/spec/fixtures/response/list.xml +0 -18
  40. data/spec/fixtures/response/multi_ref.xml +0 -39
  41. data/spec/fixtures/response/no_body.xml +0 -1
  42. data/spec/fixtures/response/soap_fault.xml +0 -8
  43. data/spec/fixtures/response/soap_fault12.xml +0 -18
  44. data/spec/fixtures/response/soap_fault_funky.xml +0 -8
  45. data/spec/fixtures/response/taxcloud.xml +0 -1
  46. data/spec/fixtures/ssl/client_cert.pem +0 -16
  47. data/spec/fixtures/ssl/client_encrypted_key.pem +0 -30
  48. data/spec/fixtures/ssl/client_encrypted_key_cert.pem +0 -24
  49. data/spec/fixtures/ssl/client_key.pem +0 -15
  50. data/spec/fixtures/wsdl/authentication.xml +0 -63
  51. data/spec/fixtures/wsdl/betfair.xml +0 -2981
  52. data/spec/fixtures/wsdl/brand.xml +0 -624
  53. data/spec/fixtures/wsdl/edialog.xml +0 -15416
  54. data/spec/fixtures/wsdl/interhome.xml +0 -2137
  55. data/spec/fixtures/wsdl/lower_camel.xml +0 -52
  56. data/spec/fixtures/wsdl/multiple_namespaces.xml +0 -92
  57. data/spec/fixtures/wsdl/multiple_types.xml +0 -60
  58. data/spec/fixtures/wsdl/no_message_tag.xml +0 -1267
  59. data/spec/fixtures/wsdl/taxcloud.xml +0 -934
  60. data/spec/fixtures/wsdl/team_software.xml +0 -1
  61. data/spec/fixtures/wsdl/vies.xml +0 -176
  62. data/spec/fixtures/wsdl/wasmuth.xml +0 -153
  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/support/application.rb +0 -82
  69. data/spec/integration/support/server.rb +0 -84
  70. data/spec/integration/temperature_example_spec.rb +0 -46
  71. data/spec/integration/zipcode_example_spec.rb +0 -42
  72. data/spec/savon/builder_spec.rb +0 -137
  73. data/spec/savon/client_spec.rb +0 -271
  74. data/spec/savon/core_ext/string_spec.rb +0 -37
  75. data/spec/savon/features/message_tag_spec.rb +0 -61
  76. data/spec/savon/http_error_spec.rb +0 -49
  77. data/spec/savon/log_message_spec.rb +0 -50
  78. data/spec/savon/message_spec.rb +0 -70
  79. data/spec/savon/mock_spec.rb +0 -174
  80. data/spec/savon/model_spec.rb +0 -182
  81. data/spec/savon/observers_spec.rb +0 -92
  82. data/spec/savon/operation_spec.rb +0 -230
  83. data/spec/savon/options_spec.rb +0 -1104
  84. data/spec/savon/qualified_message_spec.rb +0 -101
  85. data/spec/savon/request_logger_spec.rb +0 -37
  86. data/spec/savon/request_spec.rb +0 -540
  87. data/spec/savon/response_spec.rb +0 -275
  88. data/spec/savon/soap_fault_spec.rb +0 -136
  89. data/spec/savon/softlayer_spec.rb +0 -27
  90. data/spec/spec_helper.rb +0 -30
  91. data/spec/support/adapters.rb +0 -48
  92. data/spec/support/endpoint.rb +0 -25
  93. data/spec/support/fixture.rb +0 -39
  94. data/spec/support/integration.rb +0 -9
  95. data/spec/support/stdout.rb +0 -25
data/README.md CHANGED
@@ -2,18 +2,18 @@
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
- ## Version 2
14
+ ## Installation
15
15
 
16
- Savon version 2 is available through [Rubygems](http://rubygems.org/gems/savon) and can be installed via:
16
+ Savon is available through [Rubygems](http://rubygems.org/gems/savon) and can be installed via:
17
17
 
18
18
  ```
19
19
  $ gem install savon
@@ -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.15.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,18 @@ 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
+ Every savon release is tested with contemporary supported versions of ruby. Historical compatibility information:
58
+
59
+ * `main` - same support as Ruby
60
+ * 2.15.x - MRI 3.0, 3.1, 3.2, 3.3
61
+ * 2.13.x, 2.14.x - MRI 2.7, 3.0, 3.1
62
+ * 2.12.x - MRI 2.2, 2.3, 2.4, 2.5
51
63
  * 2.11.x - MRI 2.0, 2.1, 2.2, and 2.3
52
64
 
53
- If you are running MRI 1.8.7, try the 2.6.x branch.
65
+ If you are running MRI 1.8.7, try a 2.6.x release.
66
+
67
+ Most changes are not backported to older versions of savon, or unsupported ruby versions.
54
68
 
55
69
  ## Running tests
56
70
 
@@ -64,19 +78,7 @@ $ bundle exec rspec
64
78
  * 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
79
  - See https://github.com/savonrb/savon/issues/488 for more info
66
80
 
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
81
 
75
82
  ## Documentation
76
83
 
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).
84
+ Please be sure to [read the documentation](https://www.rubydoc.info/github/savonrb/savon/).
@@ -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,25 @@ 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
+ next if identifier == 'xmlns' # Do not include xmlns namespace as this causes issues for some servers (https://github.com/savonrb/savon/issues/986)
125
125
 
126
- key = ["xmlns"]
127
- key << env_namespace if env_namespace && env_namespace != ""
128
- namespaces[key.join(":")] = SOAP_NAMESPACE[@globals[:soap_version]]
126
+ prefixed_identifier = "xmlns:#{identifier}"
127
+
128
+ next if namespaces.key?(prefixed_identifier)
129
+
130
+ namespaces[prefixed_identifier] = path
131
+ end
132
+ end
129
133
 
130
134
  namespaces
131
135
  end
@@ -160,20 +164,22 @@ module Savon
160
164
  message_tag = serialized_message_tag[1]
161
165
  @wsdl.soap_input(@operation_name.to_sym)[message_tag].each_pair do |message, type|
162
166
  break if @locals[:message].nil?
163
- message_locals = @locals[:message][message.snakecase.to_sym]
167
+ message_locals = @locals[:message][StringUtils.snakecase(message).to_sym]
164
168
  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}>"
169
+ messages += "<#{message} xsi:type=\"#{type.join(':')}\">#{message_content}</#{message}>"
166
170
  end
167
171
  messages
168
172
  end
169
173
 
170
174
  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)
175
+ wsdl_tag_name = @wsdl.document? && @wsdl.soap_input(@operation_name.to_sym)
176
+
177
+ message_tag = wsdl_tag_name.keys.first if wsdl_tag_name.is_a?(Hash)
172
178
  message_tag ||= @locals[:message_tag]
173
- message_tag ||= @wsdl.soap_input(@operation_name.to_sym) if @wsdl.document?
179
+ message_tag ||= wsdl_tag_name
174
180
  message_tag ||= Gyoku.xml_tag(@operation_name, :key_converter => @globals[:convert_request_keys_to])
175
181
 
176
- @message_tag = message_tag.to_sym
182
+ message_tag.to_sym
177
183
  end
178
184
 
179
185
  def message_attributes
@@ -227,5 +233,69 @@ module Savon
227
233
  end
228
234
  end
229
235
 
236
+ def build_xml
237
+ tag(builder, :Envelope, namespaces_with_globals) do |xml|
238
+ tag(xml, :Header, header_attributes) { xml << header.to_s } unless header.empty?
239
+ tag(xml, :Body, body_attributes) do
240
+ if @globals[:no_message_tag]
241
+ xml << message.to_s
242
+ else
243
+ xml.tag!(*namespaced_message_tag) { xml << body_message }
244
+ end
245
+ end
246
+ end
247
+ end
248
+
249
+ def build_multipart_message(message_xml)
250
+ multipart_message = init_multipart_message(message_xml)
251
+ add_attachments_to_multipart_message(multipart_message)
252
+
253
+ multipart_message.ready_to_send!
254
+
255
+ # the mail.body.encoded algorithm reorders the parts, default order is [ "text/plain", "text/enriched", "text/html" ]
256
+ # should redefine the sort order, because the soap request xml should be the first
257
+ multipart_message.body.set_sort_order [ "text/xml" ]
258
+
259
+ multipart_message.body.encoded(multipart_message.content_transfer_encoding)
260
+ end
261
+
262
+ def init_multipart_message(message_xml)
263
+ multipart_message = Mail.new
264
+ xml_part = Mail::Part.new do
265
+ content_type 'text/xml'
266
+ body message_xml
267
+ # in Content-Type the start parameter is recommended (RFC 2387)
268
+ content_id '<soap-request-body@soap>'
269
+ end
270
+ multipart_message.add_part xml_part
271
+
272
+ #request.headers["Content-Type"] = "multipart/related; boundary=\"#{multipart_message.body.boundary}\"; type=\"text/xml\"; start=\"#{xml_part.content_id}\""
273
+ @multipart = {
274
+ multipart_boundary: multipart_message.body.boundary,
275
+ start: xml_part.content_id,
276
+ }
277
+
278
+ multipart_message
279
+ end
280
+
281
+ def add_attachments_to_multipart_message(multipart_message)
282
+ if @locals[:attachments].is_a? Hash
283
+ # hash example: { 'att1' => '/path/to/att1', 'att2' => '/path/to/att2' }
284
+ @locals[:attachments].each do |identifier, attachment|
285
+ add_attachment_to_multipart_message(multipart_message, attachment, identifier)
286
+ end
287
+ elsif @locals[:attachments].is_a? Array
288
+ # array example: [ '/path/to/att1', '/path/to/att2' ]
289
+ # array example: [ { filename: 'att1.xml', content: '<x/>' }, { filename: 'att2.xml', content: '<y/>' } ]
290
+ @locals[:attachments].each do |attachment|
291
+ add_attachment_to_multipart_message(multipart_message, attachment, attachment.is_a?(String) ? File.basename(attachment) : attachment[:filename])
292
+ end
293
+ end
294
+ end
295
+
296
+ def add_attachment_to_multipart_message(multipart_message, attachment, identifier)
297
+ multipart_message.add_file attachment.clone
298
+ multipart_message.parts.last.content_id = multipart_message.parts.last.content_location = identifier.to_s
299
+ end
230
300
  end
231
301
  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"
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
 
@@ -28,7 +29,7 @@ module Savon
28
29
  # Defines a class-level SOAP operation.
29
30
  def define_class_operation(operation)
30
31
  class_operation_module.module_eval %{
31
- def #{operation.to_s.snakecase}(locals = {})
32
+ def #{StringUtils.snakecase(operation.to_s)}(locals = {})
32
33
  client.call #{operation.inspect}, locals
33
34
  end
34
35
  }
@@ -37,8 +38,8 @@ module Savon
37
38
  # Defines an instance-level SOAP operation.
38
39
  def define_instance_operation(operation)
39
40
  instance_operation_module.module_eval %{
40
- def #{operation.to_s.snakecase}(locals = {})
41
- self.class.#{operation.to_s.snakecase} locals
41
+ def #{StringUtils.snakecase(operation.to_s)}(locals = {})
42
+ self.class.#{StringUtils.snakecase(operation.to_s)} locals
42
43
  end
43
44
  }
44
45
  end
@@ -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,12 +79,13 @@ 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,
84
86
  :strip_namespaces => true,
85
87
  :delete_namespace_attributes => false,
86
- :convert_response_tags_to => lambda { |tag| tag.snakecase.to_sym},
88
+ :convert_response_tags_to => lambda { |tag| StringUtils.snakecase(tag).to_sym},
87
89
  :convert_attributes_to => lambda { |k,v| [k,v] },
88
90
  :multipart => false,
89
91
  :adapter => nil,
@@ -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)