savon 2.17.4 → 3.0.0.rc1
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 +17 -115
- data/README.md +52 -52
- data/Rakefile +2 -6
- data/lib/savon/block_interface.rb +4 -3
- data/lib/savon/builder.rb +37 -60
- data/lib/savon/client.rb +13 -44
- data/lib/savon/header.rb +40 -87
- data/lib/savon/http_error.rb +9 -9
- data/lib/savon/log_message.rb +3 -2
- data/lib/savon/message.rb +7 -6
- data/lib/savon/mock/expectation.rb +24 -38
- data/lib/savon/mock/spec_helper.rb +11 -7
- data/lib/savon/mock.rb +0 -1
- data/lib/savon/model.rb +25 -25
- data/lib/savon/operation.rb +92 -112
- data/lib/savon/options.rb +175 -276
- data/lib/savon/qualified_message.rb +1 -2
- data/lib/savon/request.rb +147 -0
- data/lib/savon/request_logger.rb +57 -0
- data/lib/savon/response.rb +31 -45
- data/lib/savon/soap_fault.rb +6 -6
- data/lib/savon/string_utils.rb +4 -3
- data/lib/savon/version.rb +1 -2
- data/lib/savon.rb +11 -2
- metadata +130 -71
- data/lib/savon/addressing.rb +0 -60
- data/lib/savon/effective_options.rb +0 -124
- data/lib/savon/faraday_migration_hint.rb +0 -186
- data/lib/savon/transport/faraday.rb +0 -101
- data/lib/savon/transport/httpi.rb +0 -138
- data/lib/savon/transport/logging.rb +0 -60
- data/lib/savon/transport/response.rb +0 -73
data/lib/savon/client.rb
CHANGED
|
@@ -1,26 +1,19 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
2
|
require "savon/operation"
|
|
4
|
-
require "savon/
|
|
5
|
-
require "savon/transport/faraday"
|
|
3
|
+
require "savon/request"
|
|
6
4
|
require "savon/options"
|
|
7
5
|
require "savon/block_interface"
|
|
8
6
|
require "wasabi"
|
|
9
7
|
|
|
10
8
|
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.
|
|
16
9
|
class Client
|
|
10
|
+
|
|
17
11
|
def initialize(globals = {}, &block)
|
|
18
|
-
unless globals.
|
|
12
|
+
unless globals.kind_of? Hash
|
|
19
13
|
raise_version1_initialize_error! globals
|
|
20
14
|
end
|
|
21
15
|
|
|
22
16
|
set_globals(globals, block)
|
|
23
|
-
@globals.validate_transport!
|
|
24
17
|
|
|
25
18
|
unless wsdl_or_endpoint_and_namespace_specified?
|
|
26
19
|
raise_initialization_error!
|
|
@@ -31,25 +24,13 @@ module Savon
|
|
|
31
24
|
|
|
32
25
|
attr_reader :globals, :wsdl
|
|
33
26
|
|
|
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
|
-
|
|
46
27
|
def operations
|
|
47
28
|
raise_missing_wsdl_error! unless @wsdl.document?
|
|
48
29
|
@wsdl.soap_actions
|
|
49
30
|
end
|
|
50
31
|
|
|
51
32
|
def operation(operation_name)
|
|
52
|
-
Operation.create(operation_name, @wsdl, @globals
|
|
33
|
+
Operation.create(operation_name, @wsdl, @globals)
|
|
53
34
|
end
|
|
54
35
|
|
|
55
36
|
def call(operation_name, locals = {}, &block)
|
|
@@ -67,15 +48,6 @@ module Savon
|
|
|
67
48
|
|
|
68
49
|
private
|
|
69
50
|
|
|
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
|
-
|
|
79
51
|
def set_globals(globals, block)
|
|
80
52
|
globals = GlobalOptions.new(globals)
|
|
81
53
|
BlockInterface.new(globals).evaluate(block) if block
|
|
@@ -86,16 +58,12 @@ module Savon
|
|
|
86
58
|
def build_wsdl_document
|
|
87
59
|
@wsdl = Wasabi::Document.new
|
|
88
60
|
|
|
89
|
-
@wsdl.document
|
|
90
|
-
@wsdl.endpoint
|
|
91
|
-
@wsdl.namespace
|
|
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
|
|
92
65
|
|
|
93
|
-
|
|
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
|
|
66
|
+
@wsdl.request = WSDLRequest.new(@globals).build
|
|
99
67
|
end
|
|
100
68
|
|
|
101
69
|
def wsdl_or_endpoint_and_namespace_specified?
|
|
@@ -104,9 +72,9 @@ module Savon
|
|
|
104
72
|
|
|
105
73
|
def raise_version1_initialize_error!(object)
|
|
106
74
|
raise InitializationError,
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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"
|
|
110
78
|
end
|
|
111
79
|
|
|
112
80
|
def raise_initialization_error!
|
|
@@ -120,5 +88,6 @@ module Savon
|
|
|
120
88
|
def raise_missing_wsdl_error!
|
|
121
89
|
raise "Unable to inspect the service without a WSDL document."
|
|
122
90
|
end
|
|
91
|
+
|
|
123
92
|
end
|
|
124
93
|
end
|
data/lib/savon/header.rb
CHANGED
|
@@ -1,136 +1,89 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
2
|
require "akami"
|
|
4
3
|
require "gyoku"
|
|
5
|
-
require "
|
|
4
|
+
require "securerandom"
|
|
6
5
|
|
|
7
6
|
module Savon
|
|
8
|
-
# Assembles the SOAP +Header+ element of a request envelope and renders it to
|
|
9
|
-
# XML.
|
|
10
|
-
#
|
|
11
|
-
# A SOAP Header carries a request's out-of-band metadata. This object is
|
|
12
|
-
# responsible for every header block Savon can emit, concatenated in this
|
|
13
|
-
# order:
|
|
14
|
-
#
|
|
15
|
-
# 1. application headers supplied by the caller (the +soap_header+ option),
|
|
16
|
-
# 2. the WS-Addressing headers, rendered by {Savon::Addressing},
|
|
17
|
-
# 3. the WS-Security header with UsernameToken credentials, a +wsu:Timestamp+
|
|
18
|
-
# and an XML Signature (OASIS WSS SOAP Message Security 1.1).
|
|
19
|
-
#
|
|
20
|
-
# WS-Security markup is delegated to Akami and Hash-to-XML conversion to Gyoku.
|
|
21
7
|
class Header
|
|
22
|
-
# @param globals [Savon::GlobalOptions] client-level options, read for the
|
|
23
|
-
# Gyoku key converter (+:convert_request_keys_to+) and the WS-Addressing
|
|
24
|
-
# toggle (+:use_wsa_headers+)
|
|
25
|
-
# @param effective [Savon::EffectiveOptions] resolves the WSSE, soap_header,
|
|
26
|
-
# soap_action and endpoint values for the request
|
|
27
|
-
def initialize(globals, effective)
|
|
28
|
-
@gyoku_options = { key_converter: globals[:convert_request_keys_to] }
|
|
29
|
-
|
|
30
|
-
@wsse_auth = effective.wsse_auth
|
|
31
|
-
@wsse_timestamp = effective.wsse_timestamp
|
|
32
|
-
@wsse_signature = effective.wsse_signature
|
|
33
|
-
@soap_header = effective.soap_header
|
|
34
|
-
|
|
35
|
-
@addressing = build_addressing(globals, effective)
|
|
36
|
-
@header = build
|
|
37
|
-
end
|
|
38
8
|
|
|
39
|
-
|
|
9
|
+
def initialize(globals, locals)
|
|
10
|
+
@gyoku_options = { :key_converter => globals[:convert_request_keys_to] }
|
|
11
|
+
|
|
12
|
+
@wsse_auth = locals[:wsse_auth].nil? ? globals[:wsse_auth] : locals[:wsse_auth]
|
|
13
|
+
@wsse_timestamp = locals[:wsse_timestamp].nil? ? globals[:wsse_timestamp] : locals[:wsse_timestamp]
|
|
14
|
+
@wsse_signature = locals[:wsse_signature].nil? ? globals[:wsse_signature] : locals[:wsse_signature]
|
|
40
15
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
16
|
+
@global_header = globals[:soap_header]
|
|
17
|
+
@local_header = locals[:soap_header]
|
|
18
|
+
|
|
19
|
+
@globals = globals
|
|
20
|
+
@locals = locals
|
|
21
|
+
|
|
22
|
+
@header = build
|
|
47
23
|
end
|
|
48
24
|
|
|
49
|
-
|
|
50
|
-
|
|
25
|
+
attr_reader :local_header, :global_header, :gyoku_options,
|
|
26
|
+
:wsse_auth, :wsse_timestamp, :wsse_signature
|
|
27
|
+
|
|
51
28
|
def empty?
|
|
52
29
|
@header.empty?
|
|
53
30
|
end
|
|
54
31
|
|
|
55
|
-
# Returns the rendered SOAP header XML. The header is built once during
|
|
56
|
-
# construction and cached, so this is a cheap, repeatable read.
|
|
57
|
-
#
|
|
58
|
-
# @return [String] the header XML (may be empty)
|
|
59
32
|
def to_s
|
|
60
33
|
@header
|
|
61
34
|
end
|
|
62
35
|
|
|
63
36
|
private
|
|
64
37
|
|
|
65
|
-
# Constructs the WS-Addressing section from the resolved request values.
|
|
66
|
-
# Resolving the endpoint raises without a WSDL document and without a
|
|
67
|
-
# global +:endpoint+, so action and endpoint are only resolved when the
|
|
68
|
-
# +:use_wsa_headers+ global asks for the headers.
|
|
69
|
-
#
|
|
70
|
-
# @return [Savon::Addressing]
|
|
71
|
-
def build_addressing(globals, effective)
|
|
72
|
-
return Addressing.new(enabled: false) unless globals[:use_wsa_headers]
|
|
73
|
-
|
|
74
|
-
Addressing.new(
|
|
75
|
-
enabled: true,
|
|
76
|
-
action: effective.soap_action,
|
|
77
|
-
to: effective.endpoint
|
|
78
|
-
)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# Concatenates the three header sections in document order: caller content,
|
|
82
|
-
# WS-Addressing, then WSSE security.
|
|
83
|
-
#
|
|
84
|
-
# @return [String]
|
|
85
38
|
def build
|
|
86
|
-
build_header +
|
|
39
|
+
build_header + build_wsa_header + build_wsse_header
|
|
87
40
|
end
|
|
88
41
|
|
|
89
|
-
# Renders the resolved soap_header. {Savon::EffectiveOptions#soap_header}
|
|
90
|
-
# merges the global and local values, so there is nothing to combine here.
|
|
91
|
-
# Hash-to-XML conversion only. Strings pass through.
|
|
92
|
-
#
|
|
93
|
-
# @return [String]
|
|
94
42
|
def build_header
|
|
95
|
-
|
|
43
|
+
header =
|
|
44
|
+
if global_header.kind_of?(Hash) && local_header.kind_of?(Hash)
|
|
45
|
+
global_header.merge(local_header)
|
|
46
|
+
elsif local_header
|
|
47
|
+
local_header
|
|
48
|
+
else
|
|
49
|
+
global_header
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
convert_to_xml(header)
|
|
96
53
|
end
|
|
97
54
|
|
|
98
|
-
# Builds the WS-Security header via Akami, or an empty string when no WSSE
|
|
99
|
-
# content (credentials, timestamp or signature) applies.
|
|
100
|
-
#
|
|
101
|
-
# @return [String]
|
|
102
55
|
def build_wsse_header
|
|
103
56
|
wsse_header = akami
|
|
104
57
|
wsse_header.respond_to?(:to_xml) ? wsse_header.to_xml : ""
|
|
105
58
|
end
|
|
106
59
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
+
})
|
|
67
|
+
end
|
|
68
|
+
|
|
112
69
|
def convert_to_xml(hash_or_string)
|
|
113
|
-
if hash_or_string.
|
|
70
|
+
if hash_or_string.kind_of? Hash
|
|
114
71
|
Gyoku.xml(hash_or_string, gyoku_options)
|
|
115
72
|
else
|
|
116
73
|
hash_or_string.to_s
|
|
117
74
|
end
|
|
118
75
|
end
|
|
119
76
|
|
|
120
|
-
# Configures an Akami WSSE object from the resolved WSSE options. Sets the
|
|
121
|
-
# UsernameToken credentials, enables the wsu:Timestamp, and attaches the XML
|
|
122
|
-
# Signature when a document has been assigned to it.
|
|
123
|
-
#
|
|
124
|
-
# @return [Akami::WSSE] configured, not yet serialized
|
|
125
77
|
def akami
|
|
126
78
|
wsse = Akami.wsse
|
|
127
79
|
wsse.credentials(*wsse_auth) if wsse_auth
|
|
128
80
|
wsse.timestamp = wsse_timestamp if wsse_timestamp
|
|
129
|
-
if wsse_signature
|
|
81
|
+
if wsse_signature && wsse_signature.have_document?
|
|
130
82
|
wsse.signature = wsse_signature
|
|
131
83
|
end
|
|
132
84
|
|
|
133
85
|
wsse
|
|
134
86
|
end
|
|
87
|
+
|
|
135
88
|
end
|
|
136
89
|
end
|
data/lib/savon/http_error.rb
CHANGED
|
@@ -2,26 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
module Savon
|
|
4
4
|
class HTTPError < Error
|
|
5
|
+
|
|
5
6
|
def self.present?(http)
|
|
6
|
-
http.
|
|
7
|
+
!http.success?
|
|
7
8
|
end
|
|
8
9
|
|
|
9
|
-
def initialize(
|
|
10
|
-
@
|
|
10
|
+
def initialize(http)
|
|
11
|
+
@http = http
|
|
11
12
|
end
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
@transport_response
|
|
15
|
-
end
|
|
14
|
+
attr_reader :http
|
|
16
15
|
|
|
17
16
|
def to_s
|
|
18
|
-
String.new("HTTP error (#{http.
|
|
19
|
-
str_error << ": #{http.body}" unless http.body.empty?
|
|
17
|
+
String.new("HTTP error (#{@http.status})").tap do |str_error|
|
|
18
|
+
str_error << ": #{@http.body}" unless @http.body.empty?
|
|
20
19
|
end
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
def to_hash
|
|
24
|
-
{ code
|
|
23
|
+
{ :code => @http.status, :headers => @http.headers, :body => @http.body }
|
|
25
24
|
end
|
|
25
|
+
|
|
26
26
|
end
|
|
27
27
|
end
|
data/lib/savon/log_message.rb
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
2
|
require "nokogiri"
|
|
4
3
|
|
|
5
4
|
module Savon
|
|
6
5
|
class LogMessage
|
|
6
|
+
|
|
7
7
|
def initialize(message, filters = [], pretty_print = false)
|
|
8
8
|
@message = message
|
|
9
9
|
@filters = filters
|
|
@@ -46,7 +46,8 @@ module Savon
|
|
|
46
46
|
end
|
|
47
47
|
|
|
48
48
|
def nokogiri_options
|
|
49
|
-
@pretty_print ? { indent
|
|
49
|
+
@pretty_print ? { :indent => 2 } : { :save_with => Nokogiri::XML::Node::SaveOptions::AS_XML }
|
|
50
50
|
end
|
|
51
|
+
|
|
51
52
|
end
|
|
52
53
|
end
|
data/lib/savon/message.rb
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
2
|
require "savon/qualified_message"
|
|
4
3
|
require "gyoku"
|
|
5
4
|
|
|
6
5
|
module Savon
|
|
7
6
|
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,20 +18,21 @@ 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.kind_of? 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
|
|
29
|
-
namespace
|
|
30
|
-
key_converter
|
|
31
|
-
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
|
+
|
|
36
37
|
end
|
|
37
38
|
end
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "httpi"
|
|
4
|
-
require "savon/transport/response"
|
|
2
|
+
require "faraday"
|
|
5
3
|
|
|
6
4
|
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.
|
|
13
5
|
class MockExpectation
|
|
6
|
+
|
|
14
7
|
def initialize(operation_name)
|
|
15
|
-
@expected = { operation_name
|
|
8
|
+
@expected = { :operation_name => operation_name }
|
|
16
9
|
@actual = nil
|
|
17
10
|
end
|
|
18
11
|
|
|
@@ -22,15 +15,15 @@ module Savon
|
|
|
22
15
|
end
|
|
23
16
|
|
|
24
17
|
def returns(response)
|
|
25
|
-
response = { code
|
|
18
|
+
response = { :code => 200, :headers => {}, :body => response } if response.kind_of?(String)
|
|
26
19
|
@response = response
|
|
27
20
|
self
|
|
28
21
|
end
|
|
29
22
|
|
|
30
|
-
def actual(operation_name,
|
|
23
|
+
def actual(operation_name, builder, globals, locals)
|
|
31
24
|
@actual = {
|
|
32
|
-
operation_name
|
|
33
|
-
message
|
|
25
|
+
:operation_name => operation_name,
|
|
26
|
+
:message => locals[:message]
|
|
34
27
|
}
|
|
35
28
|
end
|
|
36
29
|
|
|
@@ -44,49 +37,42 @@ module Savon
|
|
|
44
37
|
verify_message!
|
|
45
38
|
end
|
|
46
39
|
|
|
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
|
|
51
40
|
def response!
|
|
52
41
|
unless @response
|
|
53
42
|
raise ExpectationError, "This expectation was not set up with a response."
|
|
54
43
|
end
|
|
55
|
-
|
|
56
|
-
|
|
44
|
+
env = Faraday::Env.from(status: @response[:code], response_headers: @response[:headers], response_body: @response[:body])
|
|
45
|
+
Faraday::Response.new(env)
|
|
57
46
|
end
|
|
58
47
|
|
|
59
48
|
private
|
|
60
49
|
|
|
61
50
|
def verify_operation_name!
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
|
66
55
|
end
|
|
67
56
|
|
|
68
57
|
def verify_message!
|
|
69
58
|
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."
|
|
70
62
|
|
|
71
|
-
|
|
63
|
+
actual_message = " with this message: #{@actual[:message].inspect}" if @actual[:message]
|
|
64
|
+
actual_message ||= " with no message."
|
|
72
65
|
|
|
73
|
-
|
|
74
|
-
|
|
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}"
|
|
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
|
|
81
69
|
end
|
|
82
70
|
|
|
83
71
|
def equals_except_any(msg_expected, msg_real)
|
|
84
|
-
|
|
85
|
-
return
|
|
86
|
-
return false if msg_expected.nil? || msg_real.nil? # If both are nil has returned true
|
|
87
|
-
|
|
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
|
|
88
74
|
msg_expected.each do |key, expected_value|
|
|
89
|
-
next if expected_value == :any &&
|
|
75
|
+
next if (expected_value == :any && msg_real.include?(key))
|
|
90
76
|
return false if expected_value != msg_real[key]
|
|
91
77
|
end
|
|
92
78
|
true
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
2
|
require "savon/mock"
|
|
4
3
|
|
|
5
4
|
module Savon
|
|
6
5
|
module SpecHelper
|
|
6
|
+
|
|
7
7
|
class Interface
|
|
8
|
+
|
|
8
9
|
def mock!
|
|
9
10
|
Savon.observers << self
|
|
10
11
|
end
|
|
@@ -26,12 +27,14 @@ module Savon
|
|
|
26
27
|
def notify(operation_name, builder, globals, locals)
|
|
27
28
|
expectation = expectations.shift
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
expectation.actual(operation_name, builder, globals, locals)
|
|
30
|
+
if expectation
|
|
31
|
+
expectation.actual(operation_name, builder, globals, locals)
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
33
|
+
expectation.verify!
|
|
34
|
+
expectation.response!
|
|
35
|
+
else
|
|
36
|
+
raise ExpectationError, "Unexpected request to the #{operation_name.inspect} operation."
|
|
37
|
+
end
|
|
35
38
|
rescue ExpectationError
|
|
36
39
|
@expectations.clear
|
|
37
40
|
raise
|
|
@@ -39,12 +42,12 @@ module Savon
|
|
|
39
42
|
|
|
40
43
|
def verify!
|
|
41
44
|
return if expectations.empty?
|
|
42
|
-
|
|
43
45
|
expectations.each(&:verify!)
|
|
44
46
|
rescue ExpectationError
|
|
45
47
|
@expectations.clear
|
|
46
48
|
raise
|
|
47
49
|
end
|
|
50
|
+
|
|
48
51
|
end
|
|
49
52
|
|
|
50
53
|
def savon
|
|
@@ -55,5 +58,6 @@ module Savon
|
|
|
55
58
|
super if defined? super
|
|
56
59
|
savon.verify!
|
|
57
60
|
end
|
|
61
|
+
|
|
58
62
|
end
|
|
59
63
|
end
|
data/lib/savon/mock.rb
CHANGED
data/lib/savon/model.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
2
|
module Savon
|
|
4
3
|
module Model
|
|
4
|
+
|
|
5
5
|
def self.extended(base)
|
|
6
6
|
base.setup
|
|
7
7
|
end
|
|
@@ -28,30 +28,26 @@ module Savon
|
|
|
28
28
|
|
|
29
29
|
# Defines a class-level SOAP operation.
|
|
30
30
|
def define_class_operation(operation)
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
31
|
+
class_operation_module.module_eval %{
|
|
32
|
+
def #{StringUtils.snakecase(operation.to_s)}(locals = {})
|
|
33
|
+
client.call #{operation.inspect}, locals
|
|
34
|
+
end
|
|
35
|
+
}
|
|
36
36
|
end
|
|
37
37
|
|
|
38
38
|
# Defines an instance-level SOAP operation.
|
|
39
39
|
def define_instance_operation(operation)
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
# Returns the generated Ruby method name for a SOAP operation.
|
|
48
|
-
def operation_method_name(operation)
|
|
49
|
-
StringUtils.snakecase(operation.to_s).to_sym
|
|
40
|
+
instance_operation_module.module_eval %{
|
|
41
|
+
def #{StringUtils.snakecase(operation.to_s)}(locals = {})
|
|
42
|
+
self.class.#{StringUtils.snakecase(operation.to_s)} locals
|
|
43
|
+
end
|
|
44
|
+
}
|
|
50
45
|
end
|
|
51
46
|
|
|
52
47
|
# Class methods.
|
|
53
48
|
def class_operation_module
|
|
54
|
-
@class_operation_module ||= Module.new
|
|
49
|
+
@class_operation_module ||= Module.new {
|
|
50
|
+
|
|
55
51
|
def client(globals = {})
|
|
56
52
|
@client ||= Savon::Client.new(globals)
|
|
57
53
|
rescue InitializationError
|
|
@@ -64,22 +60,26 @@ module Savon
|
|
|
64
60
|
|
|
65
61
|
def raise_initialization_error!
|
|
66
62
|
raise InitializationError,
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
63
|
+
"Expected the model to be initialized with either a WSDL document or the SOAP endpoint and target namespace options.\n" \
|
|
64
|
+
"Make sure to setup the model by calling the .client class method before calling the .global method.\n\n" \
|
|
65
|
+
"client(wsdl: '/Users/me/project/service.wsdl') # to use a local WSDL document\n" \
|
|
66
|
+
"client(wsdl: 'http://example.com?wsdl') # to use a remote WSDL document\n" \
|
|
67
|
+
"client(endpoint: 'http://example.com', namespace: 'http://v1.example.com') # if you don't have a WSDL document"
|
|
72
68
|
end
|
|
73
|
-
|
|
69
|
+
|
|
70
|
+
}.tap { |mod| extend(mod) }
|
|
74
71
|
end
|
|
75
72
|
|
|
76
73
|
# Instance methods.
|
|
77
74
|
def instance_operation_module
|
|
78
|
-
@instance_operation_module ||= Module.new
|
|
75
|
+
@instance_operation_module ||= Module.new {
|
|
76
|
+
|
|
79
77
|
def client
|
|
80
78
|
self.class.client
|
|
81
79
|
end
|
|
82
|
-
|
|
80
|
+
|
|
81
|
+
}.tap { |mod| include(mod) }
|
|
83
82
|
end
|
|
83
|
+
|
|
84
84
|
end
|
|
85
85
|
end
|