savon 2.12.0 → 2.15.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 (95) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +107 -76
  3. data/README.md +15 -19
  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/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)
@@ -1,14 +1,20 @@
1
+ # frozen_string_literal: true
1
2
  require "nori"
2
3
  require "savon/soap_fault"
3
4
  require "savon/http_error"
4
5
 
5
6
  module Savon
6
7
  class Response
8
+ CRLF = /\r\n/
9
+ WSP = /[#{%Q|\x9\x20|}]/
7
10
 
8
11
  def initialize(http, globals, locals)
9
12
  @http = http
10
13
  @globals = globals
11
14
  @locals = locals
15
+ @attachments = []
16
+ @xml = ''
17
+ @has_parsed_body = false
12
18
 
13
19
  build_soap_and_http_errors!
14
20
  raise_soap_and_http_errors! if @globals[:raise_errors]
@@ -48,12 +54,17 @@ module Savon
48
54
  result.kind_of?(Array) ? result.compact : [result].compact
49
55
  end
50
56
 
51
- def hash
52
- @hash ||= nori.parse(xml)
57
+ def full_hash
58
+ @full_hash ||= nori.parse(xml)
53
59
  end
54
60
 
55
61
  def xml
56
- @http.body
62
+ if multipart?
63
+ parse_body unless @has_parsed_body
64
+ @xml
65
+ else
66
+ @http.body
67
+ end
57
68
  end
58
69
 
59
70
  alias_method :to_xml, :xml
@@ -68,14 +79,50 @@ module Savon
68
79
  end
69
80
 
70
81
  def find(*path)
71
- envelope = nori.find(hash, 'Envelope')
82
+ envelope = nori.find(full_hash, 'Envelope')
72
83
  raise_invalid_response_error! unless envelope.is_a?(Hash)
73
84
 
74
85
  nori.find(envelope, *path)
75
86
  end
76
87
 
88
+ def attachments
89
+ if multipart?
90
+ parse_body unless @has_parsed_body
91
+ @attachments
92
+ else
93
+ []
94
+ end
95
+ end
96
+
97
+ def multipart?
98
+ !(http.headers['content-type'] =~ /^multipart/im).nil?
99
+ end
100
+
77
101
  private
78
102
 
103
+ def boundary
104
+ return unless multipart?
105
+ Mail::Field.new('content-type', http.headers['content-type']).parameters['boundary']
106
+ end
107
+
108
+ def parse_body
109
+ http.body.force_encoding Encoding::ASCII_8BIT
110
+ parts = http.body.split(/(?:\A|\r\n)--#{Regexp.escape(boundary)}(?=(?:--)?\s*$)/)
111
+ parts[1..-1].to_a.each_with_index do |part, index|
112
+ header_part, body_part = part.lstrip.split(/#{CRLF}#{CRLF}|#{CRLF}#{WSP}*#{CRLF}(?!#{WSP})/m, 2)
113
+ section = Mail::Part.new(
114
+ body: body_part
115
+ )
116
+ section.header = header_part
117
+ if index == 0
118
+ @xml = section.body.to_s
119
+ else
120
+ @attachments << section
121
+ end
122
+ end
123
+ @has_parsed_body = true
124
+ end
125
+
79
126
  def build_soap_and_http_errors!
80
127
  @soap_fault = SOAPFault.new(@http, nori, xml) if soap_fault?
81
128
  @http_error = HTTPError.new(@http) if http_error?
@@ -1,10 +1,11 @@
1
+ # frozen_string_literal: true
1
2
  module Savon
2
3
  class SOAPFault < Error
3
4
 
4
5
  def self.present?(http, xml = nil)
5
6
  xml ||= http.body
6
7
  fault_node = xml.include?("Fault>")
7
- soap1_fault = xml.include?("faultcode>") && xml.include?("faultstring>")
8
+ soap1_fault = xml.match(/faultcode\/?\>/) && xml.match(/faultstring\/?\>/)
8
9
  soap2_fault = xml.include?("Code>") && xml.include?("Reason>")
9
10
 
10
11
  fault_node && (soap1_fault || soap2_fault)
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Savon
4
+ module StringUtils
5
+ def self.snakecase(inputstring)
6
+ str = inputstring.dup
7
+ str.gsub! /::/, '/'
8
+ str.gsub! /([A-Z]+)([A-Z][a-z])/, '\1_\2'
9
+ str.gsub! /([a-z\d])([A-Z])/, '\1_\2'
10
+ str.tr! ".", "_"
11
+ str.tr! "-", "_"
12
+ str.downcase!
13
+ str
14
+ end
15
+ end
16
+ end
17
+
data/lib/savon/version.rb CHANGED
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module Savon
2
- VERSION = '2.12.0'
3
+ VERSION = '2.15.0'
3
4
  end