savon 2.15.1 → 2.17.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: df435a9b3a37060f3dbbfe5ba2f8b36c33ad66b6474afa08771396b6812fb4b3
4
- data.tar.gz: ac8cccabad1588894528b1d2ad073e576224a6b4a5b25d10a0437fb5a807e538
3
+ metadata.gz: e494bfc08714bcce13c230100c2d5677ac2b40cca1a5afb2312534b41c4c81e9
4
+ data.tar.gz: fb3b31cb0684a0e7742f247f121a9d3d5749fc932ea089ae26bf4af59fd48e24
5
5
  SHA512:
6
- metadata.gz: 79a1dd54bd1d171afc117927c9b5419f7c6d2e2a5e2b1cfad0072397e786f5932bb3180b40f7ea9d55a08194c1554afddf3efe558959157cdcc7e8b13243beb9
7
- data.tar.gz: 9e9f944a60f6f655da4abfed31020befb23a9823bb3437ccb46e527b1b8e091fac81c776fc3057f88985a80bbae3c8bb9746bf8c1e9c3966ac8992bc0ee7f936
6
+ metadata.gz: b79c099e954cb9bb176a898f515d12bcaaa557d5d15045155029e6322f3121b5304daf76e07d0927323f3e6481b43aee754d0bfa516eb0890807f12ecc6c6053
7
+ data.tar.gz: f39e6773c7c20d254857a420f6c54f38b74e1e0e64cf2e51254ca4ab43b482e2bb910860fc1f86ce86c18653e940caae115b5ace3dcad8c83db819370dd77b8c
data/CHANGELOG.md CHANGED
@@ -1,7 +1,33 @@
1
1
  # Savon changelog
2
2
 
3
- ## Unreleased
4
- * Add your PR changelog line here
3
+ ## 2.17.0 (2026-05-19)
4
+
5
+ **Add opt-in Faraday transport**
6
+
7
+ Callers who set `transport: :faraday` get a memoized `Faraday::Connection` via `client.faraday` and full control over middleware, SSL, auth, and timeouts. Callers who do not set this option see no behavior change. HTTPI remains the default for 2.x.
8
+
9
+ * Add: `transport: :faraday` global option. Defaults to `:httpi` (#992).
10
+ * Add: `client.faraday` returns a memoized `Faraday::Connection` for configuring middleware, SSL, auth, and timeouts when using the Faraday transport.
11
+ * Add: `Savon.client` raises if `transport: :faraday` is set but the faraday gem is not installed, or if any httpi-specific global option (`proxy`, timeouts, `ssl`, auth, `adapter`) is set alongside it. All conflicts are reported with their Faraday equivalents.
12
+ * Change: Observers must return `Savon::Transport::Response` (or `nil`) instead of `HTTPI::Response`. Returning `HTTPI::Response` still works but emits a deprecation warning.
13
+ * Unblocks:
14
+ * redirect following for WSDL fetches via `faraday-follow-redirects` middleware (#1033, savonrb/wasabi#18)
15
+ * digest authentication via `faraday-digestauth` middleware (#1021, savonrb/httpi#250)
16
+ * proxy authentication with special characters in passwords (#941)
17
+ * and setting an `Accept` header for WSDL requests from Rails apps (savonrb/wasabi#115)
18
+
19
+ ## 2.16.0 (2026-05-18)
20
+
21
+ **Restore compatibility**
22
+
23
+ If you stayed on 2.12.1 because a later version broke something, this release is for you. The fixes below target the most commonly reported upgrade blockers. Existing code should work without modification.
24
+
25
+ * Fix: Restore `Savon::Response#hash` removed in 2.14.0 (#985). Callers on 2.12.1 that use `response.hash` get the soap body back instead of Ruby's integer object id. A deprecation warning is emitted on each call. Use `#full_hash` going forward.
26
+ * Fix: Require wasabi >= 5.1.0 (#1015, #1016). Wasabi 4.x used message names as soap body element names and SOAPAction header values instead of operation names (savonrb/wasabi#122), causing servers to return a fault or reject the action for operations whose message name carried an `In` suffix.
27
+ * Fix: Stop dumping all WSDL namespaces into every soap envelope (#1014, #942). 2.13.0 injected every namespace from the entire WSDL document into each request, including structural ones that have no place in a request body. Strict servers reject envelopes with unexpected or duplicate declarations.
28
+ * Fix: Raise a proper `SOAPFault` instead of a raw exception when `soap:Fault` contains invalid encoding (#923).
29
+ * Fix: `SOAPFault.present?` was ignoring its `xml` argument and always operating on the instance's own body.
30
+ * Change: Added Ruby 3.4 (#1024) and Ruby 4.0 (#1039) to the CI test matrix.
5
31
 
6
32
  ## 2.15.1 (2024-07-08)
7
33
 
data/README.md CHANGED
@@ -3,11 +3,10 @@
3
3
  Heavy metal SOAP client
4
4
 
5
5
  [Documentation](https://www.rubydoc.info/gems/savon/) | [Support](https://stackoverflow.com/questions/tagged/savon) |
6
- [Mailing list](https://groups.google.com/forum/#!forum/savonrb) | [Twitter](http://twitter.com/savonrb)
6
+ [Mailing list](https://groups.google.com/forum/#!forum/savonrb)
7
7
 
8
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
- [![Code Climate](https://codeclimate.com/github/savonrb/savon.svg)](https://codeclimate.com/github/savonrb/savon)
11
10
  [![Coverage Status](https://coveralls.io/repos/savonrb/savon/badge.svg)](https://coveralls.io/r/savonrb/savon)
12
11
 
13
12
 
@@ -52,6 +51,27 @@ response.body
52
51
  For more examples, you should check out the
53
52
  [integration tests](https://github.com/savonrb/savon/tree/version2/spec/integration).
54
53
 
54
+ ## Faraday transport
55
+
56
+ Savon uses HTTPI for HTTP by default. To opt into Faraday instead, add `faraday` to your Gemfile and set `transport: :faraday`:
57
+
58
+ ```ruby
59
+ client = Savon.client(
60
+ transport: :faraday,
61
+ wsdl: "http://service.example.com?wsdl"
62
+ )
63
+ ```
64
+
65
+ Configure SSL, auth, timeouts, middleware, and anything else transport-related on the `Faraday::Connection` before making calls:
66
+
67
+ ```ruby
68
+ client.faraday.ssl.verify = false
69
+ client.faraday.options.timeout = 30
70
+ client.faraday.options.open_timeout = 5
71
+ client.faraday.headers["Authorization"] = "Bearer #{token}"
72
+ client.faraday.use Faraday::Response::Logger
73
+ ```
74
+
55
75
  ## Ruby version support
56
76
 
57
77
  Every savon release is tested with contemporary supported versions of ruby. Historical compatibility information:
data/Rakefile CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require "bundler/gem_tasks"
2
4
  require "rspec/core/rake_task"
3
5
 
@@ -10,5 +12,7 @@ RSpec::Core::RakeTask.new "spec:integration" do |t|
10
12
  t.pattern = "spec/integration/**/*_spec.rb"
11
13
  end
12
14
 
13
- task :default => :spec
14
- task :test => :spec
15
+ desc "Alias for spec task"
16
+ task test: :spec
17
+
18
+ task default: :spec
@@ -1,16 +1,16 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Savon
3
4
  class BlockInterface
4
-
5
5
  def initialize(target)
6
6
  @target = target
7
7
  end
8
8
 
9
9
  def evaluate(block)
10
- if block.arity > 0
10
+ if block.arity.positive?
11
11
  block.call(@target)
12
12
  else
13
- @original = eval("self", block.binding)
13
+ @original = eval("self", block.binding, __FILE__, __LINE__)
14
14
  instance_eval(&block)
15
15
  end
16
16
  end
@@ -22,6 +22,5 @@ module Savon
22
22
  rescue NoMethodError
23
23
  @original.send(method, *args, &block)
24
24
  end
25
-
26
25
  end
27
26
  end
data/lib/savon/builder.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "savon/header"
3
4
  require "savon/message"
4
5
  require "nokogiri"
@@ -12,12 +13,12 @@ module Savon
12
13
  SCHEMA_TYPES = {
13
14
  "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema",
14
15
  "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance"
15
- }
16
+ }.freeze
16
17
 
17
18
  SOAP_NAMESPACE = {
18
19
  1 => "http://schemas.xmlsoap.org/soap/envelope/",
19
20
  2 => "http://www.w3.org/2003/05/soap-envelope"
20
- }
21
+ }.freeze
21
22
 
22
23
  WSA_NAMESPACE = "http://www.w3.org/2005/08/addressing"
23
24
 
@@ -34,7 +35,7 @@ module Savon
34
35
  end
35
36
 
36
37
  def pretty
37
- Nokogiri.XML(to_s).to_xml(:indent => 2)
38
+ Nokogiri.XML(to_s).to_xml(indent: 2)
38
39
  end
39
40
 
40
41
  def build_document
@@ -71,33 +72,36 @@ module Savon
71
72
 
72
73
  def to_s
73
74
  return @locals[:xml] if @locals.include? :xml
75
+
74
76
  build_document
75
77
  end
76
78
 
77
79
  private
78
80
 
79
81
  def convert_type_definitions_to_hash
80
- @wsdl.type_definitions.inject({}) do |memo, (path, type)|
82
+ @wsdl.type_definitions.each_with_object({}) do |(path, type), memo|
81
83
  memo[path] = type
82
- memo
83
84
  end
84
85
  end
85
86
 
86
87
  def convert_type_namespaces_to_hash
87
- @wsdl.type_namespaces.inject({}) do |memo, (path, uri)|
88
+ @wsdl.type_namespaces.each_with_object({}) do |(path, uri), memo|
88
89
  key, value = use_namespace(path, uri)
89
90
  memo[key] = value
90
- memo
91
91
  end
92
92
  end
93
93
 
94
94
  def use_namespace(path, uri)
95
95
  @internal_namespace_count ||= 0
96
96
 
97
- unless identifier = namespace_by_uri(uri)
98
- identifier = "ins#{@internal_namespace_count}"
97
+ unless (identifier = namespace_by_uri(uri))
98
+ wsdl_identifier = @wsdl.document? ? @wsdl.parser.namespaces.key(uri) : nil
99
+ # The prefix may already be taken by the target namespace or a user-supplied
100
+ # :namespaces override - fall back to ins0, ins1... rather than overwriting it.
101
+ wsdl_identifier = nil if wsdl_identifier && namespaces.key?("xmlns:#{wsdl_identifier}")
102
+ identifier = wsdl_identifier || "ins#{@internal_namespace_count}"
99
103
  namespaces["xmlns:#{identifier}"] = uri
100
- @internal_namespace_count += 1
104
+ @internal_namespace_count += 1 unless wsdl_identifier
101
105
  end
102
106
 
103
107
  [path, identifier]
@@ -116,21 +120,9 @@ module Savon
116
120
  @globals[:namespace] || @wsdl.namespace
117
121
 
118
122
  # check env_namespace
119
- namespaces["xmlns#{env_namespace && env_namespace != "" ? ":#{env_namespace}" : ''}"] =
123
+ namespaces["xmlns#{env_namespace && env_namespace != '' ? ":#{env_namespace}" : ''}"] =
120
124
  SOAP_NAMESPACE[@globals[:soap_version]]
121
125
 
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
-
126
- prefixed_identifier = "xmlns:#{identifier}"
127
-
128
- next if namespaces.key?(prefixed_identifier)
129
-
130
- namespaces[prefixed_identifier] = path
131
- end
132
- end
133
-
134
126
  namespaces
135
127
  end
136
128
  end
@@ -145,8 +137,9 @@ module Savon
145
137
 
146
138
  def namespaced_message_tag
147
139
  tag_name = message_tag
148
- return [tag_name] if @wsdl.document? and @wsdl.soap_input(@operation_name.to_sym).is_a?(Hash)
149
- if namespace_identifier == nil
140
+ return [tag_name] if @wsdl.document? && @wsdl.soap_input(@operation_name.to_sym).is_a?(Hash)
141
+
142
+ if namespace_identifier.nil?
150
143
  [tag_name, message_attributes]
151
144
  elsif @used_namespaces[[tag_name.to_s]]
152
145
  [@used_namespaces[[tag_name.to_s]], tag_name, message_attributes]
@@ -164,6 +157,7 @@ module Savon
164
157
  message_tag = serialized_message_tag[1]
165
158
  @wsdl.soap_input(@operation_name.to_sym)[message_tag].each_pair do |message, type|
166
159
  break if @locals[:message].nil?
160
+
167
161
  message_locals = @locals[:message][StringUtils.snakecase(message).to_sym]
168
162
  message_content = Message.new(message_tag, namespace_identifier, @types, @used_namespaces, message_locals, :unqualified, @globals[:convert_request_keys_to], @globals[:unwrap]).to_s
169
163
  messages += "<#{message} xsi:type=\"#{type.join(':')}\">#{message_content}</#{message}>"
@@ -177,7 +171,7 @@ module Savon
177
171
  message_tag = wsdl_tag_name.keys.first if wsdl_tag_name.is_a?(Hash)
178
172
  message_tag ||= @locals[:message_tag]
179
173
  message_tag ||= wsdl_tag_name
180
- message_tag ||= Gyoku.xml_tag(@operation_name, :key_converter => @globals[:convert_request_keys_to])
174
+ message_tag ||= Gyoku.xml_tag(@operation_name, key_converter: @globals[:convert_request_keys_to])
181
175
 
182
176
  message_tag.to_sym
183
177
  end
@@ -187,7 +181,7 @@ module Savon
187
181
  end
188
182
 
189
183
  def body_message
190
- if @wsdl.document? and @wsdl.soap_input(@operation_name.to_sym).is_a?(Hash)
184
+ if @wsdl.document? && @wsdl.soap_input(@operation_name.to_sym).is_a?(Hash)
191
185
  serialized_messages
192
186
  else
193
187
  message.to_s
@@ -221,7 +215,7 @@ module Savon
221
215
 
222
216
  def builder
223
217
  builder = ::Builder::XmlMarkup.new
224
- builder.instruct!(:xml, :encoding => @globals[:encoding])
218
+ builder.instruct!(:xml, encoding: @globals[:encoding])
225
219
  builder
226
220
  end
227
221
 
@@ -254,7 +248,7 @@ module Savon
254
248
 
255
249
  # the mail.body.encoded algorithm reorders the parts, default order is [ "text/plain", "text/enriched", "text/html" ]
256
250
  # should redefine the sort order, because the soap request xml should be the first
257
- multipart_message.body.set_sort_order [ "text/xml" ]
251
+ multipart_message.body.set_sort_order ["text/xml"]
258
252
 
259
253
  multipart_message.body.encoded(multipart_message.content_transfer_encoding)
260
254
  end
@@ -269,10 +263,10 @@ module Savon
269
263
  end
270
264
  multipart_message.add_part xml_part
271
265
 
272
- #request.headers["Content-Type"] = "multipart/related; boundary=\"#{multipart_message.body.boundary}\"; type=\"text/xml\"; start=\"#{xml_part.content_id}\""
266
+ # request.headers["Content-Type"] = "multipart/related; boundary=\"#{multipart_message.body.boundary}\"; type=\"text/xml\"; start=\"#{xml_part.content_id}\""
273
267
  @multipart = {
274
268
  multipart_boundary: multipart_message.body.boundary,
275
- start: xml_part.content_id,
269
+ start: xml_part.content_id
276
270
  }
277
271
 
278
272
  multipart_message
data/lib/savon/client.rb CHANGED
@@ -1,19 +1,26 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "savon/operation"
3
- require "savon/request"
4
+ require "savon/transport/httpi"
5
+ require "savon/transport/faraday"
4
6
  require "savon/options"
5
7
  require "savon/block_interface"
6
8
  require "wasabi"
7
9
 
8
10
  module Savon
11
+ # The main entry point for Savon.
12
+ #
13
+ # Holds global configuration, owns the WSDL document, and dispatches
14
+ # named operations. A single Client instance is typically shared across
15
+ # multiple calls to the same service.
9
16
  class Client
10
-
11
17
  def initialize(globals = {}, &block)
12
- unless globals.kind_of? Hash
18
+ unless globals.is_a? Hash
13
19
  raise_version1_initialize_error! globals
14
20
  end
15
21
 
16
22
  set_globals(globals, block)
23
+ @globals.validate_transport!
17
24
 
18
25
  unless wsdl_or_endpoint_and_namespace_specified?
19
26
  raise_initialization_error!
@@ -24,13 +31,25 @@ module Savon
24
31
 
25
32
  attr_reader :globals, :wsdl
26
33
 
34
+ # Returns the memoized Faraday::Connection for this client.
35
+ # Callers use this to configure middleware, SSL, auth, timeouts, and any
36
+ # other transport-level concern before making calls.
37
+ # Raises ArgumentError if transport is not :faraday.
38
+ def faraday
39
+ unless @globals[:transport] == :faraday
40
+ raise ArgumentError, "client.faraday is only available when transport: :faraday is set"
41
+ end
42
+
43
+ @faraday ||= ::Faraday.new
44
+ end
45
+
27
46
  def operations
28
47
  raise_missing_wsdl_error! unless @wsdl.document?
29
48
  @wsdl.soap_actions
30
49
  end
31
50
 
32
51
  def operation(operation_name)
33
- Operation.create(operation_name, @wsdl, @globals)
52
+ Operation.create(operation_name, @wsdl, @globals, build_transport)
34
53
  end
35
54
 
36
55
  def call(operation_name, locals = {}, &block)
@@ -48,6 +67,15 @@ module Savon
48
67
 
49
68
  private
50
69
 
70
+ # Builds the transport for a single operation.
71
+ def build_transport
72
+ if @globals[:transport] == :faraday
73
+ Transport::Faraday.new(faraday, @globals)
74
+ else
75
+ Transport::HTTPI.new(@globals)
76
+ end
77
+ end
78
+
51
79
  def set_globals(globals, block)
52
80
  globals = GlobalOptions.new(globals)
53
81
  BlockInterface.new(globals).evaluate(block) if block
@@ -58,12 +86,16 @@ module Savon
58
86
  def build_wsdl_document
59
87
  @wsdl = Wasabi::Document.new
60
88
 
61
- @wsdl.document = @globals[:wsdl] if @globals.include? :wsdl
62
- @wsdl.endpoint = @globals[:endpoint] if @globals.include? :endpoint
63
- @wsdl.namespace = @globals[:namespace] if @globals.include? :namespace
64
- @wsdl.adapter = @globals[:adapter] if @globals.include? :adapter
89
+ @wsdl.document = @globals[:wsdl] if @globals.include? :wsdl
90
+ @wsdl.endpoint = @globals[:endpoint] if @globals.include? :endpoint
91
+ @wsdl.namespace = @globals[:namespace] if @globals.include? :namespace
65
92
 
66
- @wsdl.request = WSDLRequest.new(@globals).build
93
+ if @globals[:transport] == :faraday
94
+ @wsdl.request = faraday
95
+ else
96
+ @wsdl.adapter = @globals[:adapter] if @globals.include? :adapter
97
+ @wsdl.request = Transport::HTTPI.new(@globals).wsdl_request
98
+ end
67
99
  end
68
100
 
69
101
  def wsdl_or_endpoint_and_namespace_specified?
@@ -72,9 +104,9 @@ module Savon
72
104
 
73
105
  def raise_version1_initialize_error!(object)
74
106
  raise InitializationError,
75
- "Some code tries to initialize Savon with the #{object.inspect} (#{object.class}) \n" \
76
- "Savon 2 expects a Hash of options for creating a new client and executing requests.\n" \
77
- "Please read the updated documentation for version 2: http://savonrb.com/version2.html"
107
+ "Some code tries to initialize Savon with the #{object.inspect} (#{object.class}) \n" \
108
+ "Savon 2 expects a Hash of options for creating a new client and executing requests.\n" \
109
+ "Please read the updated documentation for version 2: http://savonrb.com/version2.html"
78
110
  end
79
111
 
80
112
  def raise_initialization_error!
@@ -88,6 +120,5 @@ module Savon
88
120
  def raise_missing_wsdl_error!
89
121
  raise "Unable to inspect the service without a WSDL document."
90
122
  end
91
-
92
123
  end
93
124
  end
data/lib/savon/header.rb CHANGED
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "akami"
3
4
  require "gyoku"
4
5
  require "securerandom"
5
6
 
6
7
  module Savon
7
8
  class Header
8
-
9
9
  def initialize(globals, locals)
10
- @gyoku_options = { :key_converter => globals[:convert_request_keys_to] }
10
+ @gyoku_options = { key_converter: globals[:convert_request_keys_to] }
11
11
 
12
12
  @wsse_auth = locals[:wsse_auth].nil? ? globals[:wsse_auth] : locals[:wsse_auth]
13
13
  @wsse_timestamp = locals[:wsse_timestamp].nil? ? globals[:wsse_timestamp] : locals[:wsse_timestamp]
@@ -41,7 +41,7 @@ module Savon
41
41
 
42
42
  def build_header
43
43
  header =
44
- if global_header.kind_of?(Hash) && local_header.kind_of?(Hash)
44
+ if global_header.is_a?(Hash) && local_header.is_a?(Hash)
45
45
  global_header.merge(local_header)
46
46
  elsif local_header
47
47
  local_header
@@ -58,16 +58,17 @@ module Savon
58
58
  end
59
59
 
60
60
  def build_wsa_header
61
- return '' unless @globals[:use_wsa_headers]
62
- convert_to_xml({
63
- 'wsa:Action' => @locals[:soap_action],
64
- 'wsa:To' => @globals[:endpoint],
65
- 'wsa:MessageID' => "urn:uuid:#{SecureRandom.uuid}"
66
- })
61
+ return '' unless @globals[:use_wsa_headers]
62
+
63
+ convert_to_xml({
64
+ 'wsa:Action' => @locals[:soap_action],
65
+ 'wsa:To' => @globals[:endpoint],
66
+ 'wsa:MessageID' => "urn:uuid:#{SecureRandom.uuid}"
67
+ })
67
68
  end
68
69
 
69
70
  def convert_to_xml(hash_or_string)
70
- if hash_or_string.kind_of? Hash
71
+ if hash_or_string.is_a? Hash
71
72
  Gyoku.xml(hash_or_string, gyoku_options)
72
73
  else
73
74
  hash_or_string.to_s
@@ -78,12 +79,11 @@ module Savon
78
79
  wsse = Akami.wsse
79
80
  wsse.credentials(*wsse_auth) if wsse_auth
80
81
  wsse.timestamp = wsse_timestamp if wsse_timestamp
81
- if wsse_signature && wsse_signature.have_document?
82
+ if wsse_signature&.have_document?
82
83
  wsse.signature = wsse_signature
83
84
  end
84
85
 
85
86
  wsse
86
87
  end
87
-
88
88
  end
89
89
  end
@@ -2,7 +2,6 @@
2
2
 
3
3
  module Savon
4
4
  class HTTPError < Error
5
-
6
5
  def self.present?(http)
7
6
  http.error?
8
7
  end
@@ -20,8 +19,7 @@ module Savon
20
19
  end
21
20
 
22
21
  def to_hash
23
- { :code => @http.code, :headers => @http.headers, :body => @http.body }
22
+ { code: @http.code, headers: @http.headers, body: @http.body }
24
23
  end
25
-
26
24
  end
27
25
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "nokogiri"
3
4
 
4
5
  module Savon
5
6
  class LogMessage
6
-
7
7
  def initialize(message, filters = [], pretty_print = false)
8
8
  @message = message
9
9
  @filters = filters
@@ -46,8 +46,7 @@ module Savon
46
46
  end
47
47
 
48
48
  def nokogiri_options
49
- @pretty_print ? { :indent => 2 } : { :save_with => Nokogiri::XML::Node::SaveOptions::AS_XML }
49
+ @pretty_print ? { indent: 2 } : { save_with: Nokogiri::XML::Node::SaveOptions::AS_XML }
50
50
  end
51
-
52
51
  end
53
52
  end
data/lib/savon/message.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "savon/qualified_message"
3
4
  require "gyoku"
4
5
 
5
6
  module Savon
6
7
  class Message
7
-
8
8
  def initialize(message_tag, namespace_identifier, types, used_namespaces, message, element_form_default, key_converter, unwrap)
9
9
  @message_tag = message_tag
10
10
  @namespace_identifier = namespace_identifier
@@ -18,21 +18,20 @@ module Savon
18
18
  end
19
19
 
20
20
  def to_s
21
- return @message.to_s unless @message.kind_of? Hash
21
+ return @message.to_s unless @message.is_a? Hash
22
22
 
23
23
  if @element_form_default == :qualified
24
24
  @message = QualifiedMessage.new(@types, @used_namespaces, @key_converter).to_hash(@message, [@message_tag.to_s])
25
25
  end
26
26
 
27
27
  gyoku_options = {
28
- :element_form_default => @element_form_default,
29
- :namespace => @namespace_identifier,
30
- :key_converter => @key_converter,
31
- :unwrap => @unwrap
28
+ element_form_default: @element_form_default,
29
+ namespace: @namespace_identifier,
30
+ key_converter: @key_converter,
31
+ unwrap: @unwrap
32
32
  }
33
33
 
34
34
  Gyoku.xml(@message, gyoku_options)
35
35
  end
36
-
37
36
  end
38
37
  end
@@ -1,11 +1,18 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "httpi"
4
+ require "savon/transport/response"
3
5
 
4
6
  module Savon
7
+ # A single test expectation set up by Savon's mock interface.
8
+ # One expectation covers one operation call in one test.
9
+ #
10
+ # Records the expected operation name and message, captures what was
11
+ # actually called, and either returns a synthetic response or raises
12
+ # an error on mismatch.
5
13
  class MockExpectation
6
-
7
14
  def initialize(operation_name)
8
- @expected = { :operation_name => operation_name }
15
+ @expected = { operation_name: operation_name }
9
16
  @actual = nil
10
17
  end
11
18
 
@@ -15,15 +22,15 @@ module Savon
15
22
  end
16
23
 
17
24
  def returns(response)
18
- response = { :code => 200, :headers => {}, :body => response } if response.kind_of?(String)
25
+ response = { code: 200, headers: {}, body: response } if response.is_a?(String)
19
26
  @response = response
20
27
  self
21
28
  end
22
29
 
23
- def actual(operation_name, builder, globals, locals)
30
+ def actual(operation_name, _builder, _globals, locals)
24
31
  @actual = {
25
- :operation_name => operation_name,
26
- :message => locals[:message]
32
+ operation_name: operation_name,
33
+ message: locals[:message]
27
34
  }
28
35
  end
29
36
 
@@ -37,45 +44,52 @@ module Savon
37
44
  verify_message!
38
45
  end
39
46
 
47
+ # Builds and returns a Transport::Response from the configured response hash.
48
+ #
49
+ # @return [Transport::Response]
50
+ # @raise [ExpectationError] if no response was configured for this expectation
40
51
  def response!
41
52
  unless @response
42
53
  raise ExpectationError, "This expectation was not set up with a response."
43
54
  end
44
55
 
45
- HTTPI::Response.new(@response[:code], @response[:headers], @response[:body])
56
+ Transport::Response.new(@response[:code], @response[:headers], @response[:body])
46
57
  end
47
58
 
48
59
  private
49
60
 
50
61
  def verify_operation_name!
51
- unless @expected[:operation_name] == @actual[:operation_name]
52
- raise ExpectationError, "Expected a request to the #{@expected[:operation_name].inspect} operation.\n" \
53
- "Received a request to the #{@actual[:operation_name].inspect} operation instead."
54
- end
62
+ return if @expected[:operation_name] == @actual[:operation_name]
63
+
64
+ raise ExpectationError, "Expected a request to the #{@expected[:operation_name].inspect} operation.\n" \
65
+ "Received a request to the #{@actual[:operation_name].inspect} operation instead."
55
66
  end
56
67
 
57
68
  def verify_message!
58
69
  return if @expected[:message].eql? :any
59
- unless equals_except_any(@expected[:message], @actual[:message])
60
- expected_message = " with this message: #{@expected[:message].inspect}" if @expected[:message]
61
- expected_message ||= " with no message."
62
70
 
63
- actual_message = " with this message: #{@actual[:message].inspect}" if @actual[:message]
64
- actual_message ||= " with no message."
71
+ return if equals_except_any(@expected[:message], @actual[:message])
65
72
 
66
- raise ExpectationError, "Expected a request to the #{@expected[:operation_name].inspect} operation\n#{expected_message}\n" \
67
- "Received a request to the #{@actual[:operation_name].inspect} operation\n#{actual_message}"
68
- end
73
+ expected_message = " with this message: #{@expected[:message].inspect}" if @expected[:message]
74
+ expected_message ||= " with no message."
75
+
76
+ actual_message = " with this message: #{@actual[:message].inspect}" if @actual[:message]
77
+ actual_message ||= " with no message."
78
+
79
+ raise ExpectationError, "Expected a request to the #{@expected[:operation_name].inspect} operation\n#{expected_message}\n" \
80
+ "Received a request to the #{@actual[:operation_name].inspect} operation\n#{actual_message}"
69
81
  end
70
82
 
71
83
  def equals_except_any(msg_expected, msg_real)
72
- return true if msg_expected === msg_real
73
- return false if (msg_expected.nil? || msg_real.nil?) # If both are nil has returned true
84
+ # === allows RSpec matchers (e.g. include(:key)) to be used as expected values
85
+ return true if msg_expected === msg_real # rubocop:disable Style/CaseEquality
86
+ return false if msg_expected.nil? || msg_real.nil? # If both are nil has returned true
87
+
74
88
  msg_expected.each do |key, expected_value|
75
- next if (expected_value == :any && msg_real.include?(key))
89
+ next if expected_value == :any && msg_real.include?(key)
76
90
  return false if expected_value != msg_real[key]
77
91
  end
78
- return true
92
+ true
79
93
  end
80
94
  end
81
95
  end