savon 2.11.2 → 2.14.0

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 (94) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +112 -73
  3. data/README.md +25 -16
  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 +71 -2
  17. data/lib/savon/qualified_message.rb +5 -4
  18. data/lib/savon/request.rb +18 -3
  19. data/lib/savon/request_logger.rb +8 -2
  20. data/lib/savon/response.rb +52 -5
  21. data/lib/savon/soap_fault.rb +2 -3
  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 +62 -97
  26. data/.gitignore +0 -14
  27. data/.travis.yml +0 -19
  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/soap_fault.xml +0 -8
  42. data/spec/fixtures/response/soap_fault12.xml +0 -18
  43. data/spec/fixtures/response/soap_fault_funky.xml +0 -8
  44. data/spec/fixtures/response/taxcloud.xml +0 -1
  45. data/spec/fixtures/ssl/client_cert.pem +0 -16
  46. data/spec/fixtures/ssl/client_encrypted_key.pem +0 -30
  47. data/spec/fixtures/ssl/client_encrypted_key_cert.pem +0 -24
  48. data/spec/fixtures/ssl/client_key.pem +0 -15
  49. data/spec/fixtures/wsdl/authentication.xml +0 -63
  50. data/spec/fixtures/wsdl/betfair.xml +0 -2981
  51. data/spec/fixtures/wsdl/brand.xml +0 -624
  52. data/spec/fixtures/wsdl/edialog.xml +0 -15416
  53. data/spec/fixtures/wsdl/interhome.xml +0 -2137
  54. data/spec/fixtures/wsdl/lower_camel.xml +0 -52
  55. data/spec/fixtures/wsdl/multiple_namespaces.xml +0 -92
  56. data/spec/fixtures/wsdl/multiple_types.xml +0 -60
  57. data/spec/fixtures/wsdl/no_message_tag.xml +0 -1267
  58. data/spec/fixtures/wsdl/taxcloud.xml +0 -934
  59. data/spec/fixtures/wsdl/team_software.xml +0 -1
  60. data/spec/fixtures/wsdl/vies.xml +0 -176
  61. data/spec/fixtures/wsdl/wasmuth.xml +0 -153
  62. data/spec/integration/centra_spec.rb +0 -66
  63. data/spec/integration/email_example_spec.rb +0 -32
  64. data/spec/integration/random_quote_spec.rb +0 -23
  65. data/spec/integration/ratp_example_spec.rb +0 -28
  66. data/spec/integration/stockquote_example_spec.rb +0 -34
  67. data/spec/integration/support/application.rb +0 -82
  68. data/spec/integration/support/server.rb +0 -84
  69. data/spec/integration/temperature_example_spec.rb +0 -46
  70. data/spec/integration/zipcode_example_spec.rb +0 -42
  71. data/spec/savon/builder_spec.rb +0 -137
  72. data/spec/savon/client_spec.rb +0 -271
  73. data/spec/savon/core_ext/string_spec.rb +0 -37
  74. data/spec/savon/features/message_tag_spec.rb +0 -61
  75. data/spec/savon/http_error_spec.rb +0 -49
  76. data/spec/savon/log_message_spec.rb +0 -50
  77. data/spec/savon/message_spec.rb +0 -70
  78. data/spec/savon/mock_spec.rb +0 -174
  79. data/spec/savon/model_spec.rb +0 -182
  80. data/spec/savon/observers_spec.rb +0 -92
  81. data/spec/savon/operation_spec.rb +0 -230
  82. data/spec/savon/options_spec.rb +0 -1075
  83. data/spec/savon/qualified_message_spec.rb +0 -68
  84. data/spec/savon/request_logger_spec.rb +0 -37
  85. data/spec/savon/request_spec.rb +0 -496
  86. data/spec/savon/response_spec.rb +0 -270
  87. data/spec/savon/soap_fault_spec.rb +0 -136
  88. data/spec/savon/softlayer_spec.rb +0 -27
  89. data/spec/spec_helper.rb +0 -30
  90. data/spec/support/adapters.rb +0 -48
  91. data/spec/support/endpoint.rb +0 -25
  92. data/spec/support/fixture.rb +0 -39
  93. data/spec/support/integration.rb +0 -9
  94. data/spec/support/stdout.rb +0 -25
@@ -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,
@@ -137,7 +139,7 @@ module Savon
137
139
 
138
140
  # Proxy server to use for all requests.
139
141
  def proxy(proxy)
140
- @options[:proxy] = proxy
142
+ @options[:proxy] = proxy unless proxy.nil?
141
143
  end
142
144
 
143
145
  # A Hash of HTTP headers.
@@ -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
@@ -268,6 +290,19 @@ module Savon
268
290
  @options[:ssl_ca_cert] = cert
269
291
  end
270
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
271
306
 
272
307
  # HTTP basic auth credentials.
273
308
  def basic_auth(*credentials)
@@ -383,6 +418,40 @@ module Savon
383
418
  @options[:attributes] = attributes
384
419
  end
385
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
+
386
455
  # Value of the SOAPAction HTTP header.
387
456
  def soap_action(soap_action)
388
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
@@ -9,7 +10,7 @@ module Savon
9
10
  end
10
11
 
11
12
  def to_hash(hash, path)
12
- return unless hash
13
+ return hash unless hash
13
14
  return hash.map { |value| to_hash(value, path) } if hash.is_a?(Array)
14
15
  return hash.to_s unless hash.is_a?(Hash)
15
16
 
@@ -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,18 +22,25 @@ 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
34
+ @http_request.auth.ssl.ciphers = @globals[:ssl_ciphers] if @globals.include? :ssl_ciphers
29
35
 
30
36
  @http_request.auth.ssl.cert_key_file = @globals[:ssl_cert_key_file] if @globals.include? :ssl_cert_key_file
31
- @http_request.auth.ssl.cert_key = @globals[:ssl_cert_key] if @globals.include? :ssl_cert_key
37
+ @http_request.auth.ssl.cert_key = @globals[:ssl_cert_key] if @globals.include? :ssl_cert_key
32
38
  @http_request.auth.ssl.cert_file = @globals[:ssl_cert_file] if @globals.include? :ssl_cert_file
33
- @http_request.auth.ssl.cert = @globals[:ssl_cert] if @globals.include? :ssl_cert
39
+ @http_request.auth.ssl.cert = @globals[:ssl_cert] if @globals.include? :ssl_cert
34
40
  @http_request.auth.ssl.ca_cert_file = @globals[:ssl_ca_cert_file] if @globals.include? :ssl_ca_cert_file
35
- @http_request.auth.ssl.ca_cert = @globals[:ssl_ca_cert] if @globals.include? :ssl_ca_cert
41
+ @http_request.auth.ssl.ca_cert_path = @globals[:ssl_ca_cert_path] if @globals.include? :ssl_ca_cert_path
42
+ @http_request.auth.ssl.ca_cert = @globals[:ssl_ca_cert] if @globals.include? :ssl_ca_cert
43
+ @http_request.auth.ssl.cert_store = @globals[:ssl_cert_store] if @globals.include? :ssl_cert_store
36
44
 
37
45
  @http_request.auth.ssl.cert_key_password = @globals[:ssl_cert_key_password] if @globals.include? :ssl_cert_key_password
38
46
  end
@@ -55,12 +63,19 @@ module Savon
55
63
  def build
56
64
  configure_proxy
57
65
  configure_timeouts
66
+ configure_headers
58
67
  configure_ssl
59
68
  configure_auth
60
69
  configure_redirect_handling
61
70
 
62
71
  @http_request
63
72
  end
73
+
74
+ private
75
+
76
+ def configure_headers
77
+ @http_request.headers = @globals[:headers] if @globals.include? :headers
78
+ end
64
79
  end
65
80
 
66
81
  class SOAPRequest < HTTPRequest
@@ -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)