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
data/lib/savon/soap_fault.rb
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
module Savon
|
|
3
4
|
class SOAPFault < Error
|
|
4
|
-
|
|
5
5
|
def self.present?(http, xml = nil)
|
|
6
6
|
body = xml || http.body
|
|
7
7
|
body = body.scrub('') unless body.valid_encoding?
|
|
8
8
|
fault_node = body.include?("Fault>")
|
|
9
|
-
soap1_fault = body.match(
|
|
9
|
+
soap1_fault = body.match(%r{faultcode/?>}) && body.match(%r{faultstring/?>})
|
|
10
10
|
soap2_fault = body.include?("Code>") && body.include?("Reason>")
|
|
11
11
|
|
|
12
12
|
fault_node && (soap1_fault || soap2_fault)
|
|
@@ -45,6 +45,5 @@ module Savon
|
|
|
45
45
|
"(#{code}) #{text}"
|
|
46
46
|
end
|
|
47
47
|
end
|
|
48
|
-
|
|
49
48
|
end
|
|
50
49
|
end
|
data/lib/savon/string_utils.rb
CHANGED
|
@@ -4,9 +4,9 @@ module Savon
|
|
|
4
4
|
module StringUtils
|
|
5
5
|
def self.snakecase(inputstring)
|
|
6
6
|
str = inputstring.dup
|
|
7
|
-
str.gsub!
|
|
8
|
-
str.gsub!
|
|
9
|
-
str.gsub!
|
|
7
|
+
str.gsub!(/::/, '/')
|
|
8
|
+
str.gsub!(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
9
|
+
str.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
|
|
10
10
|
str.tr! ".", "_"
|
|
11
11
|
str.tr! "-", "_"
|
|
12
12
|
str.downcase!
|
|
@@ -14,4 +14,3 @@ module Savon
|
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
|
-
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "savon/transport/logging"
|
|
4
|
+
require "savon/transport/response"
|
|
5
|
+
|
|
6
|
+
module Savon
|
|
7
|
+
module Transport
|
|
8
|
+
# Faraday-backed HTTP transport for the opt-in Faraday path.
|
|
9
|
+
#
|
|
10
|
+
# Encapsulates everything Faraday-specific:
|
|
11
|
+
# * header assembly (SOAP + global + local + cookies + Content-Length)
|
|
12
|
+
# * request execution via the caller-configured Faraday::Connection
|
|
13
|
+
#
|
|
14
|
+
# Transport-level concerns (SSL, auth, proxy, timeouts, middleware) are
|
|
15
|
+
# the caller's responsibility via client.faraday before any call is made.
|
|
16
|
+
class Faraday
|
|
17
|
+
include Logging
|
|
18
|
+
|
|
19
|
+
# @param connection [Faraday::Connection] the memoized connection from client.faraday
|
|
20
|
+
# @param globals [Savon::GlobalOptions] the client-level options
|
|
21
|
+
def initialize(connection, globals)
|
|
22
|
+
@connection = connection
|
|
23
|
+
@globals = globals
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Assembles headers, executes the POST via the Faraday connection, and
|
|
27
|
+
# returns a Transport::Response. Logs the outbound request and inbound
|
|
28
|
+
# response when logging is enabled.
|
|
29
|
+
#
|
|
30
|
+
# @param url [String] the SOAP endpoint URL
|
|
31
|
+
# @param soap_headers [Hash] SOAP-level headers (Content-Type, SOAPAction, etc.)
|
|
32
|
+
# @param body [String] the serialized SOAP envelope
|
|
33
|
+
# @param locals [Savon::LocalOptions] per-request options
|
|
34
|
+
# @return [Transport::Response]
|
|
35
|
+
def post(url, soap_headers, body, locals)
|
|
36
|
+
headers = build_headers(soap_headers, body, locals)
|
|
37
|
+
|
|
38
|
+
log_request(url, headers, body) if log?
|
|
39
|
+
|
|
40
|
+
faraday_response = @connection.post(url, body, headers)
|
|
41
|
+
response = Response.from_faraday(faraday_response)
|
|
42
|
+
|
|
43
|
+
log_response(response) if log?
|
|
44
|
+
|
|
45
|
+
response
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
# Merges all header sources in precedence order:
|
|
51
|
+
# locals[:headers] > globals[:headers] > soap_headers
|
|
52
|
+
# Appends Cookie from locals[:cookies] and Content-Length from body.
|
|
53
|
+
def build_headers(soap_headers, body, locals)
|
|
54
|
+
headers = {}
|
|
55
|
+
headers.merge!(@globals[:headers]) if @globals.include?(:headers)
|
|
56
|
+
headers.merge!(locals[:headers]) if locals.include?(:headers)
|
|
57
|
+
|
|
58
|
+
# soap_headers are lowest priority
|
|
59
|
+
soap_headers.each do |k, v| headers[k] ||= v end
|
|
60
|
+
|
|
61
|
+
if locals[:cookies]&.any?
|
|
62
|
+
headers["Cookie"] = locals[:cookies].map(&:name_and_value).join(";")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
headers["Content-Length"] = body.bytesize.to_s
|
|
66
|
+
headers
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "httpi"
|
|
4
|
+
require "savon/transport/logging"
|
|
5
|
+
require "savon/transport/response"
|
|
6
|
+
|
|
7
|
+
module Savon
|
|
8
|
+
module Transport
|
|
9
|
+
# HTTPI-backed HTTP transport for the default HTTP path.
|
|
10
|
+
#
|
|
11
|
+
# Encapsulates everything HTTPI-specific:
|
|
12
|
+
# * header assembly
|
|
13
|
+
# * proxy/SSL/auth/timeout configuration
|
|
14
|
+
# * request execution
|
|
15
|
+
class HTTPI
|
|
16
|
+
include Logging
|
|
17
|
+
|
|
18
|
+
# @param globals [Savon::GlobalOptions] the client-level options
|
|
19
|
+
def initialize(globals)
|
|
20
|
+
@globals = globals
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Assembles and executes a SOAP request, returning a Transport::Response.
|
|
24
|
+
# Logs the outbound request and inbound response when logging is enabled.
|
|
25
|
+
#
|
|
26
|
+
# @param url [String] the SOAP endpoint URL
|
|
27
|
+
# @param soap_headers [Hash] SOAP-level headers
|
|
28
|
+
# @param body [String] the serialized SOAP envelope
|
|
29
|
+
# @param locals [Savon::LocalOptions] per-request options
|
|
30
|
+
# @return [Transport::Response]
|
|
31
|
+
def post(url, soap_headers, body, locals)
|
|
32
|
+
http_request = to_httpi_request(url, soap_headers, body, locals)
|
|
33
|
+
|
|
34
|
+
log_request(http_request.url, http_request.headers, http_request.body) if log?
|
|
35
|
+
|
|
36
|
+
http_response = ::HTTPI.post(http_request, @globals[:adapter])
|
|
37
|
+
response = Response.from_httpi(http_response)
|
|
38
|
+
|
|
39
|
+
log_response(response) if log?
|
|
40
|
+
|
|
41
|
+
response
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Builds a fully-configured HTTPI::Request.
|
|
45
|
+
#
|
|
46
|
+
# @param url [String] the SOAP endpoint URL
|
|
47
|
+
# @param soap_headers [Hash] SOAP-level headers
|
|
48
|
+
# @param body [String] the serialized SOAP envelope
|
|
49
|
+
# @param locals [Savon::LocalOptions] per-request options
|
|
50
|
+
# @return [HTTPI::Request]
|
|
51
|
+
def to_httpi_request(url, soap_headers, body, locals)
|
|
52
|
+
headers = {}
|
|
53
|
+
headers.merge!(@globals[:headers]) if @globals.include?(:headers)
|
|
54
|
+
headers.merge!(locals[:headers]) if locals.include?(:headers)
|
|
55
|
+
|
|
56
|
+
# soap_headers are lowest priority
|
|
57
|
+
soap_headers.each do |k, v| headers[k] ||= v end
|
|
58
|
+
|
|
59
|
+
if locals[:cookies]&.any?
|
|
60
|
+
headers["Cookie"] = locals[:cookies].map(&:name_and_value).join(";")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
headers["Content-Length"] = body.bytesize.to_s
|
|
64
|
+
|
|
65
|
+
http_request = ::HTTPI::Request.new
|
|
66
|
+
http_request.url = url
|
|
67
|
+
http_request.body = body
|
|
68
|
+
http_request.headers = headers
|
|
69
|
+
configure_http_request(http_request)
|
|
70
|
+
http_request
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Returns a configured HTTPI::Request for Wasabi's WSDL resolver.
|
|
74
|
+
# Applies global headers and all transport-level options and
|
|
75
|
+
# leaves the rest to Wasabi.
|
|
76
|
+
#
|
|
77
|
+
# @return [HTTPI::Request]
|
|
78
|
+
def wsdl_request
|
|
79
|
+
http_request = ::HTTPI::Request.new
|
|
80
|
+
http_request.headers = @globals[:headers].dup if @globals.include?(:headers)
|
|
81
|
+
configure_http_request(http_request)
|
|
82
|
+
http_request
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def configure_http_request(http_request)
|
|
88
|
+
configure_proxy(http_request)
|
|
89
|
+
configure_timeouts(http_request)
|
|
90
|
+
configure_ssl(http_request)
|
|
91
|
+
configure_auth(http_request)
|
|
92
|
+
configure_redirect_handling(http_request)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def configure_proxy(http_request)
|
|
96
|
+
http_request.proxy = @globals[:proxy] if @globals.include?(:proxy)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def configure_timeouts(http_request)
|
|
100
|
+
http_request.open_timeout = @globals[:open_timeout] if @globals.include?(:open_timeout)
|
|
101
|
+
http_request.read_timeout = @globals[:read_timeout] if @globals.include?(:read_timeout)
|
|
102
|
+
http_request.write_timeout = @globals[:write_timeout] if @globals.include?(:write_timeout)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Configures SSL on the HTTPI::Request from all ssl globals.
|
|
106
|
+
# SSL option reference: https://github.com/savonrb/httpi/blob/main/lib/httpi/auth/ssl.rb
|
|
107
|
+
def configure_ssl(http_request)
|
|
108
|
+
ssl = http_request.auth.ssl
|
|
109
|
+
ssl.ssl_version = @globals[:ssl_version] if @globals.include?(:ssl_version)
|
|
110
|
+
ssl.min_version = @globals[:ssl_min_version] if @globals.include?(:ssl_min_version)
|
|
111
|
+
ssl.max_version = @globals[:ssl_max_version] if @globals.include?(:ssl_max_version)
|
|
112
|
+
ssl.verify_mode = @globals[:ssl_verify_mode] if @globals.include?(:ssl_verify_mode)
|
|
113
|
+
ssl.ciphers = @globals[:ssl_ciphers] if @globals.include?(:ssl_ciphers)
|
|
114
|
+
ssl.cert_key_file = @globals[:ssl_cert_key_file] if @globals.include?(:ssl_cert_key_file)
|
|
115
|
+
ssl.cert_key = @globals[:ssl_cert_key] if @globals.include?(:ssl_cert_key)
|
|
116
|
+
ssl.cert_file = @globals[:ssl_cert_file] if @globals.include?(:ssl_cert_file)
|
|
117
|
+
ssl.cert = @globals[:ssl_cert] if @globals.include?(:ssl_cert)
|
|
118
|
+
ssl.ca_cert_file = @globals[:ssl_ca_cert_file] if @globals.include?(:ssl_ca_cert_file)
|
|
119
|
+
ssl.ca_cert_path = @globals[:ssl_ca_cert_path] if @globals.include?(:ssl_ca_cert_path)
|
|
120
|
+
ssl.ca_cert = @globals[:ssl_ca_cert] if @globals.include?(:ssl_ca_cert)
|
|
121
|
+
ssl.cert_store = @globals[:ssl_cert_store] if @globals.include?(:ssl_cert_store)
|
|
122
|
+
ssl.cert_key_password = @globals[:ssl_cert_key_password] if @globals.include?(:ssl_cert_key_password)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def configure_auth(http_request)
|
|
126
|
+
http_request.auth.basic(*@globals[:basic_auth]) if @globals.include?(:basic_auth)
|
|
127
|
+
http_request.auth.digest(*@globals[:digest_auth]) if @globals.include?(:digest_auth)
|
|
128
|
+
http_request.auth.ntlm(*@globals[:ntlm]) if @globals.include?(:ntlm)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def configure_redirect_handling(http_request)
|
|
132
|
+
http_request.follow_redirect = @globals[:follow_redirects] if @globals.include?(:follow_redirects)
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "savon/log_message"
|
|
4
|
+
|
|
5
|
+
module Savon
|
|
6
|
+
module Transport
|
|
7
|
+
# Shared logging behaviour for HTTP transports.
|
|
8
|
+
#
|
|
9
|
+
# Expects the including class to expose @globals (Savon::GlobalOptions)
|
|
10
|
+
# so that log level, filters, and pretty-print settings can be accessed.
|
|
11
|
+
#
|
|
12
|
+
# log_request and log_response are intentionally private so that each
|
|
13
|
+
# transport drives them from its own post method.
|
|
14
|
+
module Logging
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def log?
|
|
18
|
+
@globals[:log]
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def log_headers?
|
|
22
|
+
@globals[:log_headers]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def logger
|
|
26
|
+
@globals[:logger]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Logs the outbound request at INFO (URL and optional headers)
|
|
30
|
+
# and DEBUG (filtered/pretty-printed body).
|
|
31
|
+
#
|
|
32
|
+
# @param url [String] the SOAP endpoint URL
|
|
33
|
+
# @param headers [Hash] request headers
|
|
34
|
+
# @param body [String] the serialized SOAP envelope
|
|
35
|
+
def log_request(url, headers, body)
|
|
36
|
+
logger.info do "SOAP request: #{url}" end
|
|
37
|
+
logger.info { headers_to_log(headers) } if log_headers?
|
|
38
|
+
logger.debug { body_to_log(body) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Logs the inbound response at INFO (status line)
|
|
42
|
+
# and DEBUG (headers and filtered/pretty-printed body).
|
|
43
|
+
#
|
|
44
|
+
# @param response [Transport::Response]
|
|
45
|
+
def log_response(response)
|
|
46
|
+
logger.info do "SOAP response (status #{response.code})" end
|
|
47
|
+
logger.debug { headers_to_log(response.headers) } if log_headers?
|
|
48
|
+
logger.debug { body_to_log(response.body) }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def headers_to_log(headers)
|
|
52
|
+
headers.map { |key, value| "#{key}: #{value}" }.join("\n")
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def body_to_log(body)
|
|
56
|
+
LogMessage.new(body, @globals[:filters], @globals[:pretty_print_xml]).to_s
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Savon
|
|
4
|
+
module Transport
|
|
5
|
+
# Transport-agnostic HTTP response value object.
|
|
6
|
+
#
|
|
7
|
+
# Every transport produces a Transport::Response so that higher-level code
|
|
8
|
+
# never depends on transport-specific code. Immutable once constructed.
|
|
9
|
+
class Response
|
|
10
|
+
# Creates a Transport::Response from an HTTPI::Response.
|
|
11
|
+
def self.from_httpi(httpi_response)
|
|
12
|
+
new(httpi_response.code, httpi_response.headers, httpi_response.body)
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# Creates a Transport::Response from a Faraday::Response.
|
|
16
|
+
def self.from_faraday(faraday_response)
|
|
17
|
+
new(faraday_response.status, faraday_response.headers.to_h, faraday_response.body)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param code [Integer] HTTP status code
|
|
21
|
+
# @param headers [Hash] response headers
|
|
22
|
+
# @param body [String] response body
|
|
23
|
+
def initialize(code, headers, body)
|
|
24
|
+
@code = code
|
|
25
|
+
@headers = headers
|
|
26
|
+
@body = body
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Returns the HTTP status code.
|
|
30
|
+
attr_reader :code
|
|
31
|
+
|
|
32
|
+
# Returns the response headers hash.
|
|
33
|
+
attr_reader :headers
|
|
34
|
+
|
|
35
|
+
# Returns the response body string.
|
|
36
|
+
attr_reader :body
|
|
37
|
+
|
|
38
|
+
# Returns true when the HTTP status code indicates an error (>= 300).
|
|
39
|
+
def error?
|
|
40
|
+
@code >= 300
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
data/lib/savon/version.rb
CHANGED
data/lib/savon.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
module Savon
|
|
3
2
|
|
|
3
|
+
module Savon
|
|
4
4
|
Error = Class.new(RuntimeError)
|
|
5
5
|
InitializationError = Class.new(Error)
|
|
6
6
|
UnknownOptionError = Class.new(Error)
|
|
@@ -16,11 +16,10 @@ module Savon
|
|
|
16
16
|
end
|
|
17
17
|
|
|
18
18
|
def self.notify_observers(operation_name, builder, globals, locals)
|
|
19
|
-
observers.inject(nil) do |
|
|
19
|
+
observers.inject(nil) do |_response, observer|
|
|
20
20
|
observer.notify(operation_name, builder, globals, locals)
|
|
21
21
|
end
|
|
22
22
|
end
|
|
23
|
-
|
|
24
23
|
end
|
|
25
24
|
|
|
26
25
|
require "savon/version"
|