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 +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +21 -0
- data/Rakefile +6 -2
- data/lib/savon/block_interface.rb +3 -4
- data/lib/savon/builder.rb +19 -17
- data/lib/savon/client.rb +44 -13
- data/lib/savon/header.rb +12 -12
- data/lib/savon/http_error.rb +1 -3
- data/lib/savon/log_message.rb +2 -3
- data/lib/savon/message.rb +6 -7
- data/lib/savon/mock/expectation.rb +37 -23
- data/lib/savon/mock/spec_helper.rb +7 -11
- data/lib/savon/mock.rb +1 -0
- data/lib/savon/model.rb +12 -17
- data/lib/savon/operation.rb +93 -56
- data/lib/savon/options.rb +253 -153
- data/lib/savon/qualified_message.rb +2 -1
- data/lib/savon/response.rb +25 -23
- data/lib/savon/soap_fault.rb +2 -3
- data/lib/savon/string_utils.rb +3 -4
- data/lib/savon/transport/faraday.rb +70 -0
- data/lib/savon/transport/httpi.rb +136 -0
- data/lib/savon/transport/logging.rb +60 -0
- data/lib/savon/transport/response.rb +44 -0
- data/lib/savon/version.rb +2 -1
- data/lib/savon.rb +2 -3
- metadata +78 -73
- data/lib/savon/request.rb +0 -113
- data/lib/savon/request_logger.rb +0 -54
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: e494bfc08714bcce13c230100c2d5677ac2b40cca1a5afb2312534b41c4c81e9
|
|
4
|
+
data.tar.gz: fb3b31cb0684a0e7742f247f121a9d3d5749fc932ea089ae26bf4af59fd48e24
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
|
|
14
|
-
task :
|
|
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
|
|
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(:
|
|
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.
|
|
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.
|
|
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 !=
|
|
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?
|
|
141
|
-
|
|
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, :
|
|
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?
|
|
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, :
|
|
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 [
|
|
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/
|
|
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.
|
|
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
|
|
62
|
-
@wsdl.endpoint
|
|
63
|
-
@wsdl.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
|
-
@
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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 = { :
|
|
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.
|
|
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
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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.
|
|
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
|
|
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
|
data/lib/savon/http_error.rb
CHANGED
|
@@ -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
|
-
{ :
|
|
22
|
+
{ code: @http.code, headers: @http.headers, body: @http.body }
|
|
24
23
|
end
|
|
25
|
-
|
|
26
24
|
end
|
|
27
25
|
end
|
data/lib/savon/log_message.rb
CHANGED
|
@@ -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 ? { :
|
|
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.
|
|
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
|
-
:
|
|
29
|
-
:
|
|
30
|
-
:
|
|
31
|
-
:
|
|
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 = { :
|
|
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 = { :
|
|
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,
|
|
30
|
+
def actual(operation_name, _builder, _globals, locals)
|
|
24
31
|
@actual = {
|
|
25
|
-
:
|
|
26
|
-
:
|
|
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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
64
|
-
actual_message ||= " with no message."
|
|
71
|
+
return if equals_except_any(@expected[:message], @actual[:message])
|
|
65
72
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
73
|
-
return
|
|
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
|
|
89
|
+
next if expected_value == :any && msg_real.include?(key)
|
|
76
90
|
return false if expected_value != msg_real[key]
|
|
77
91
|
end
|
|
78
|
-
|
|
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
|
-
|
|
31
|
-
|
|
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
|
-
|
|
34
|
-
|
|
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
|