savon 2.16.0 → 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: 31b29512f6e1e259e34f95ef7cc0d38dd9ac8911d0b1307ca05bf69736985cbb
4
- data.tar.gz: 3ae386e5a8ade343d99e7dc10d644499cf5315fc51d058a8e3f7cb103ad25872
3
+ metadata.gz: e494bfc08714bcce13c230100c2d5677ac2b40cca1a5afb2312534b41c4c81e9
4
+ data.tar.gz: fb3b31cb0684a0e7742f247f121a9d3d5749fc932ea089ae26bf4af59fd48e24
5
5
  SHA512:
6
- metadata.gz: e18a7a8ee40adfab9ab7e316abce340251ef30ad93dd50348640488a282aa5a462d16b46fd987414747c4efe76a04b68ec3e0e50d95de77ac331c4a630549182
7
- data.tar.gz: 686fc36d67eb071a75ce201d16ec4ba3be443c23c48db90f87ee3bece8d76f05f81768acbc182fa32ffbf1623d022d9ca3b23871aa68c8b86c6ca6bd82df049a
6
+ metadata.gz: b79c099e954cb9bb176a898f515d12bcaaa557d5d15045155029e6322f3121b5304daf76e07d0927323f3e6481b43aee754d0bfa516eb0890807f12ecc6c6053
7
+ data.tar.gz: f39e6773c7c20d254857a420f6c54f38b74e1e0e64cf2e51254ca4ab43b482e2bb910860fc1f86ce86c18653e940caae115b5ace3dcad8c83db819370dd77b8c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Savon changelog
2
2
 
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
+
3
19
  ## 2.16.0 (2026-05-18)
4
20
 
5
21
  **Restore compatibility**
data/README.md CHANGED
@@ -51,6 +51,27 @@ response.body
51
51
  For more examples, you should check out the
52
52
  [integration tests](https://github.com/savonrb/savon/tree/version2/spec/integration).
53
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
+
54
75
  ## Ruby version support
55
76
 
56
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,30 +72,29 @@ 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)
97
+ unless (identifier = namespace_by_uri(uri))
98
98
  wsdl_identifier = @wsdl.document? ? @wsdl.parser.namespaces.key(uri) : nil
99
99
  # The prefix may already be taken by the target namespace or a user-supplied
100
100
  # :namespaces override - fall back to ins0, ins1... rather than overwriting it.
@@ -120,7 +120,7 @@ module Savon
120
120
  @globals[:namespace] || @wsdl.namespace
121
121
 
122
122
  # check env_namespace
123
- namespaces["xmlns#{env_namespace && env_namespace != "" ? ":#{env_namespace}" : ''}"] =
123
+ namespaces["xmlns#{env_namespace && env_namespace != '' ? ":#{env_namespace}" : ''}"] =
124
124
  SOAP_NAMESPACE[@globals[:soap_version]]
125
125
 
126
126
  namespaces
@@ -137,8 +137,9 @@ module Savon
137
137
 
138
138
  def namespaced_message_tag
139
139
  tag_name = message_tag
140
- return [tag_name] if @wsdl.document? and @wsdl.soap_input(@operation_name.to_sym).is_a?(Hash)
141
- 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?
142
143
  [tag_name, message_attributes]
143
144
  elsif @used_namespaces[[tag_name.to_s]]
144
145
  [@used_namespaces[[tag_name.to_s]], tag_name, message_attributes]
@@ -156,6 +157,7 @@ module Savon
156
157
  message_tag = serialized_message_tag[1]
157
158
  @wsdl.soap_input(@operation_name.to_sym)[message_tag].each_pair do |message, type|
158
159
  break if @locals[:message].nil?
160
+
159
161
  message_locals = @locals[:message][StringUtils.snakecase(message).to_sym]
160
162
  message_content = Message.new(message_tag, namespace_identifier, @types, @used_namespaces, message_locals, :unqualified, @globals[:convert_request_keys_to], @globals[:unwrap]).to_s
161
163
  messages += "<#{message} xsi:type=\"#{type.join(':')}\">#{message_content}</#{message}>"
@@ -169,7 +171,7 @@ module Savon
169
171
  message_tag = wsdl_tag_name.keys.first if wsdl_tag_name.is_a?(Hash)
170
172
  message_tag ||= @locals[:message_tag]
171
173
  message_tag ||= wsdl_tag_name
172
- 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])
173
175
 
174
176
  message_tag.to_sym
175
177
  end
@@ -179,7 +181,7 @@ module Savon
179
181
  end
180
182
 
181
183
  def body_message
182
- 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)
183
185
  serialized_messages
184
186
  else
185
187
  message.to_s
@@ -213,7 +215,7 @@ module Savon
213
215
 
214
216
  def builder
215
217
  builder = ::Builder::XmlMarkup.new
216
- builder.instruct!(:xml, :encoding => @globals[:encoding])
218
+ builder.instruct!(:xml, encoding: @globals[:encoding])
217
219
  builder
218
220
  end
219
221
 
@@ -246,7 +248,7 @@ module Savon
246
248
 
247
249
  # the mail.body.encoded algorithm reorders the parts, default order is [ "text/plain", "text/enriched", "text/html" ]
248
250
  # should redefine the sort order, because the soap request xml should be the first
249
- multipart_message.body.set_sort_order [ "text/xml" ]
251
+ multipart_message.body.set_sort_order ["text/xml"]
250
252
 
251
253
  multipart_message.body.encoded(multipart_message.content_transfer_encoding)
252
254
  end
@@ -261,10 +263,10 @@ module Savon
261
263
  end
262
264
  multipart_message.add_part xml_part
263
265
 
264
- #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}\""
265
267
  @multipart = {
266
268
  multipart_boundary: multipart_message.body.boundary,
267
- start: xml_part.content_id,
269
+ start: xml_part.content_id
268
270
  }
269
271
 
270
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
@@ -1,11 +1,10 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "savon/mock"
3
4
 
4
5
  module Savon
5
6
  module SpecHelper
6
-
7
7
  class Interface
8
-
9
8
  def mock!
10
9
  Savon.observers << self
11
10
  end
@@ -27,14 +26,12 @@ module Savon
27
26
  def notify(operation_name, builder, globals, locals)
28
27
  expectation = expectations.shift
29
28
 
30
- if expectation
31
- expectation.actual(operation_name, builder, globals, locals)
29
+ raise ExpectationError, "Unexpected request to the #{operation_name.inspect} operation." unless expectation
30
+
31
+ expectation.actual(operation_name, builder, globals, locals)
32
32
 
33
- expectation.verify!
34
- expectation.response!
35
- else
36
- raise ExpectationError, "Unexpected request to the #{operation_name.inspect} operation."
37
- end
33
+ expectation.verify!
34
+ expectation.response!
38
35
  rescue ExpectationError
39
36
  @expectations.clear
40
37
  raise
@@ -42,12 +39,12 @@ module Savon
42
39
 
43
40
  def verify!
44
41
  return if expectations.empty?
42
+
45
43
  expectations.each(&:verify!)
46
44
  rescue ExpectationError
47
45
  @expectations.clear
48
46
  raise
49
47
  end
50
-
51
48
  end
52
49
 
53
50
  def savon
@@ -58,6 +55,5 @@ module Savon
58
55
  super if defined? super
59
56
  savon.verify!
60
57
  end
61
-
62
58
  end
63
59
  end
data/lib/savon/mock.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Savon
3
4
  class ExpectationError < StandardError; end
4
5
  end