savon 2.16.0 → 2.17.4
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 +110 -7
- data/README.md +54 -48
- data/Rakefile +6 -2
- data/lib/savon/addressing.rb +60 -0
- data/lib/savon/block_interface.rb +3 -4
- data/lib/savon/builder.rb +54 -23
- data/lib/savon/client.rb +44 -13
- data/lib/savon/effective_options.rb +124 -0
- data/lib/savon/faraday_migration_hint.rb +186 -0
- data/lib/savon/header.rb +87 -40
- data/lib/savon/http_error.rb +8 -8
- 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 +25 -25
- data/lib/savon/operation.rb +110 -74
- data/lib/savon/options.rb +274 -157
- data/lib/savon/qualified_message.rb +2 -1
- data/lib/savon/response.rb +40 -31
- data/lib/savon/soap_fault.rb +2 -3
- data/lib/savon/string_utils.rb +3 -4
- data/lib/savon/transport/faraday.rb +101 -0
- data/lib/savon/transport/httpi.rb +138 -0
- data/lib/savon/transport/logging.rb +60 -0
- data/lib/savon/transport/response.rb +73 -0
- data/lib/savon/version.rb +2 -1
- data/lib/savon.rb +2 -3
- metadata +85 -83
- data/lib/savon/request.rb +0 -113
- data/lib/savon/request_logger.rb +0 -54
data/lib/savon/response.rb
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
+
|
|
2
3
|
require "nori"
|
|
3
4
|
require "savon/soap_fault"
|
|
4
5
|
require "savon/http_error"
|
|
@@ -6,33 +7,37 @@ require "savon/http_error"
|
|
|
6
7
|
module Savon
|
|
7
8
|
class Response
|
|
8
9
|
CRLF = /\r\n/
|
|
9
|
-
WSP = /[
|
|
10
|
+
WSP = /[\t ]/
|
|
10
11
|
|
|
11
|
-
def initialize(
|
|
12
|
-
@
|
|
13
|
-
@globals
|
|
14
|
-
@locals
|
|
15
|
-
@attachments
|
|
16
|
-
@xml
|
|
17
|
-
@has_parsed_body
|
|
12
|
+
def initialize(transport_response, globals, locals)
|
|
13
|
+
@transport_response = transport_response
|
|
14
|
+
@globals = globals
|
|
15
|
+
@locals = locals
|
|
16
|
+
@attachments = []
|
|
17
|
+
@xml = ''
|
|
18
|
+
@has_parsed_body = false
|
|
18
19
|
|
|
19
20
|
build_soap_and_http_errors!
|
|
20
21
|
raise_soap_and_http_errors! if @globals[:raise_errors]
|
|
21
22
|
end
|
|
22
23
|
|
|
23
|
-
attr_reader :
|
|
24
|
+
attr_reader :globals, :locals, :soap_fault, :http_error
|
|
25
|
+
|
|
26
|
+
def http
|
|
27
|
+
@transport_response
|
|
28
|
+
end
|
|
24
29
|
|
|
25
30
|
def success?
|
|
26
31
|
!soap_fault? && !http_error?
|
|
27
32
|
end
|
|
28
|
-
|
|
33
|
+
alias successful? success?
|
|
29
34
|
|
|
30
35
|
def soap_fault?
|
|
31
|
-
SOAPFault.present?(
|
|
36
|
+
SOAPFault.present?(http, xml)
|
|
32
37
|
end
|
|
33
38
|
|
|
34
39
|
def http_error?
|
|
35
|
-
HTTPError.present?
|
|
40
|
+
HTTPError.present? http
|
|
36
41
|
end
|
|
37
42
|
|
|
38
43
|
def header
|
|
@@ -43,15 +48,16 @@ module Savon
|
|
|
43
48
|
find('Body')
|
|
44
49
|
end
|
|
45
50
|
|
|
46
|
-
|
|
51
|
+
alias to_hash body
|
|
47
52
|
|
|
48
53
|
def to_array(*path)
|
|
49
|
-
result = path.inject
|
|
54
|
+
result = path.inject(body) { |memo, key|
|
|
50
55
|
return [] if memo[key].nil?
|
|
56
|
+
|
|
51
57
|
memo[key]
|
|
52
|
-
|
|
58
|
+
}
|
|
53
59
|
|
|
54
|
-
result.
|
|
60
|
+
result.is_a?(Array) ? result.compact : [result].compact
|
|
55
61
|
end
|
|
56
62
|
|
|
57
63
|
def hash
|
|
@@ -68,12 +74,12 @@ module Savon
|
|
|
68
74
|
parse_body unless @has_parsed_body
|
|
69
75
|
@xml
|
|
70
76
|
else
|
|
71
|
-
|
|
77
|
+
http.body
|
|
72
78
|
end
|
|
73
79
|
end
|
|
74
80
|
|
|
75
|
-
|
|
76
|
-
|
|
81
|
+
alias to_xml xml
|
|
82
|
+
alias to_s xml
|
|
77
83
|
|
|
78
84
|
def doc
|
|
79
85
|
@doc ||= Nokogiri.XML(xml)
|
|
@@ -107,19 +113,20 @@ module Savon
|
|
|
107
113
|
|
|
108
114
|
def boundary
|
|
109
115
|
return unless multipart?
|
|
116
|
+
|
|
110
117
|
Mail::Field.new('content-type', http.headers['content-type']).parameters['boundary']
|
|
111
118
|
end
|
|
112
119
|
|
|
113
120
|
def parse_body
|
|
114
121
|
http.body.force_encoding Encoding::ASCII_8BIT
|
|
115
122
|
parts = http.body.split(/(?:\A|\r\n)--#{Regexp.escape(boundary)}(?=(?:--)?\s*$)/)
|
|
116
|
-
parts[1
|
|
123
|
+
parts[1..].to_a.each_with_index do |part, index|
|
|
117
124
|
header_part, body_part = part.lstrip.split(/#{CRLF}#{CRLF}|#{CRLF}#{WSP}*#{CRLF}(?!#{WSP})/m, 2)
|
|
118
125
|
section = Mail::Part.new(
|
|
119
126
|
body: body_part
|
|
120
127
|
)
|
|
121
128
|
section.header = header_part
|
|
122
|
-
if index
|
|
129
|
+
if index.zero?
|
|
123
130
|
@xml = section.body.to_s
|
|
124
131
|
else
|
|
125
132
|
@attachments << section
|
|
@@ -129,8 +136,8 @@ module Savon
|
|
|
129
136
|
end
|
|
130
137
|
|
|
131
138
|
def build_soap_and_http_errors!
|
|
132
|
-
@soap_fault = SOAPFault.new(
|
|
133
|
-
@http_error = HTTPError.new(
|
|
139
|
+
@soap_fault = SOAPFault.new(http, nori, xml) if soap_fault?
|
|
140
|
+
@http_error = HTTPError.new(http) if http_error?
|
|
134
141
|
end
|
|
135
142
|
|
|
136
143
|
def raise_soap_and_http_errors!
|
|
@@ -139,7 +146,7 @@ module Savon
|
|
|
139
146
|
end
|
|
140
147
|
|
|
141
148
|
def raise_invalid_response_error!
|
|
142
|
-
raise InvalidResponseError, "Unable to parse response body:\n
|
|
149
|
+
raise InvalidResponseError, "Unable to parse response body:\n#{xml.inspect}"
|
|
143
150
|
end
|
|
144
151
|
|
|
145
152
|
def xml_namespaces
|
|
@@ -150,17 +157,19 @@ module Savon
|
|
|
150
157
|
return @nori if @nori
|
|
151
158
|
|
|
152
159
|
nori_options = {
|
|
153
|
-
:
|
|
154
|
-
:
|
|
155
|
-
:
|
|
156
|
-
:
|
|
157
|
-
:
|
|
158
|
-
:
|
|
160
|
+
delete_namespace_attributes: @globals[:delete_namespace_attributes],
|
|
161
|
+
strip_namespaces: @globals[:strip_namespaces],
|
|
162
|
+
empty_tag_value: @globals[:empty_tag_value],
|
|
163
|
+
convert_dashes_to_underscores: @globals[:convert_dashes_to_underscores],
|
|
164
|
+
scrub_xml: @globals[:scrub_xml],
|
|
165
|
+
convert_tags_to: @globals[:convert_response_tags_to],
|
|
166
|
+
convert_attributes_to: @globals[:convert_attributes_to],
|
|
167
|
+
advanced_typecasting: @locals[:advanced_typecasting],
|
|
168
|
+
parser: @locals[:response_parser]
|
|
159
169
|
}
|
|
160
170
|
|
|
161
171
|
non_nil_nori_options = nori_options.reject { |_, value| value.nil? }
|
|
162
172
|
@nori = Nori.new(non_nil_nori_options)
|
|
163
173
|
end
|
|
164
|
-
|
|
165
174
|
end
|
|
166
175
|
end
|
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,101 @@
|
|
|
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)
|
|
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, locals)
|
|
37
|
+
|
|
38
|
+
log_request(url, headers, body) if log?
|
|
39
|
+
|
|
40
|
+
faraday_response = @connection.post(url, body, headers)
|
|
41
|
+
response = normalize_response(faraday_response)
|
|
42
|
+
|
|
43
|
+
log_response(response) if log?
|
|
44
|
+
response
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Normalizes a native Faraday::Response into a Transport::Response.
|
|
48
|
+
#
|
|
49
|
+
# @param faraday_response [Faraday::Response]
|
|
50
|
+
# @return [Transport::Response]
|
|
51
|
+
def normalize_response(faraday_response)
|
|
52
|
+
Response.from_faraday(faraday_response)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Parses Set-Cookie headers into a Hash of name => value. Accepts both
|
|
56
|
+
# the Array and String form. Attributes after the first ';' are discarded.
|
|
57
|
+
def self.parse_cookies(headers)
|
|
58
|
+
raw = headers["set-cookie"] || headers["Set-Cookie"]
|
|
59
|
+
return {} unless raw
|
|
60
|
+
|
|
61
|
+
raw_array = raw.is_a?(Array) ? raw : raw.split(/,\s*/)
|
|
62
|
+
raw_array.each_with_object({}) do |cookie_str, hash|
|
|
63
|
+
name_value = cookie_str.split(";", 2).first.to_s.strip
|
|
64
|
+
name, value = name_value.split("=", 2)
|
|
65
|
+
hash[name] = value if name && !name.empty?
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
private
|
|
70
|
+
|
|
71
|
+
# Merges all header sources in precedence order:
|
|
72
|
+
# locals[:headers] > globals[:headers] > soap_headers
|
|
73
|
+
# Appends Cookie from locals[:cookies].
|
|
74
|
+
def build_headers(soap_headers, locals)
|
|
75
|
+
headers = {}
|
|
76
|
+
headers.merge!(@globals[:headers]) if @globals.include?(:headers)
|
|
77
|
+
headers.merge!(locals[:headers]) if locals.include?(:headers)
|
|
78
|
+
|
|
79
|
+
# soap_headers are lowest priority
|
|
80
|
+
soap_headers.each do |k, v| headers[k] ||= v end
|
|
81
|
+
|
|
82
|
+
cookie_header = format_cookies(locals[:cookies])
|
|
83
|
+
headers["Cookie"] = cookie_header if cookie_header
|
|
84
|
+
|
|
85
|
+
headers
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
# Builds the Cookie header from a given value.
|
|
89
|
+
# Accepts:
|
|
90
|
+
# * String - passed through verbatim
|
|
91
|
+
# * Hash - formatted as "name=value; name=value" (browser style)
|
|
92
|
+
# Returns nil when no cookies were supplied.
|
|
93
|
+
def format_cookies(cookies)
|
|
94
|
+
return nil if cookies.nil?
|
|
95
|
+
return cookies if cookies.is_a?(String)
|
|
96
|
+
|
|
97
|
+
cookies.map { |name, value| "#{name}=#{value}" }.join("; ")
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
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
|
+
httpi_response = ::HTTPI.post(http_request, @globals[:adapter])
|
|
37
|
+
response = normalize_response(httpi_response)
|
|
38
|
+
|
|
39
|
+
log_response(response) if log?
|
|
40
|
+
response
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Normalizes a native HTTPI::Response into a Transport::Response.
|
|
44
|
+
#
|
|
45
|
+
# @param httpi_response [HTTPI::Response]
|
|
46
|
+
# @return [Transport::Response]
|
|
47
|
+
def normalize_response(httpi_response)
|
|
48
|
+
Response.from_httpi(httpi_response)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Builds a fully-configured HTTPI::Request.
|
|
52
|
+
#
|
|
53
|
+
# @param url [String] the SOAP endpoint URL
|
|
54
|
+
# @param soap_headers [Hash] SOAP-level headers
|
|
55
|
+
# @param body [String] the serialized SOAP envelope
|
|
56
|
+
# @param locals [Savon::LocalOptions] per-request options
|
|
57
|
+
# @return [HTTPI::Request]
|
|
58
|
+
def to_httpi_request(url, soap_headers, body, locals)
|
|
59
|
+
headers = {}
|
|
60
|
+
headers.merge!(@globals[:headers]) if @globals.include?(:headers)
|
|
61
|
+
headers.merge!(locals[:headers]) if locals.include?(:headers)
|
|
62
|
+
|
|
63
|
+
# soap_headers are lowest priority
|
|
64
|
+
soap_headers.each do |k, v| headers[k] ||= v end
|
|
65
|
+
|
|
66
|
+
http_request = ::HTTPI::Request.new
|
|
67
|
+
http_request.url = url
|
|
68
|
+
http_request.body = body
|
|
69
|
+
http_request.headers = headers
|
|
70
|
+
http_request.set_cookies(locals[:cookies]) if locals[:cookies]
|
|
71
|
+
configure_http_request(http_request)
|
|
72
|
+
http_request
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Returns a configured HTTPI::Request for Wasabi's WSDL resolver.
|
|
76
|
+
# Applies global headers and all transport-level options and
|
|
77
|
+
# leaves the rest to Wasabi.
|
|
78
|
+
#
|
|
79
|
+
# @return [HTTPI::Request]
|
|
80
|
+
def wsdl_request
|
|
81
|
+
http_request = ::HTTPI::Request.new
|
|
82
|
+
http_request.headers = @globals[:headers].dup if @globals.include?(:headers)
|
|
83
|
+
configure_http_request(http_request)
|
|
84
|
+
http_request
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
private
|
|
88
|
+
|
|
89
|
+
def configure_http_request(http_request)
|
|
90
|
+
configure_proxy(http_request)
|
|
91
|
+
configure_timeouts(http_request)
|
|
92
|
+
configure_ssl(http_request)
|
|
93
|
+
configure_auth(http_request)
|
|
94
|
+
configure_redirect_handling(http_request)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def configure_proxy(http_request)
|
|
98
|
+
http_request.proxy = @globals[:proxy] if @globals.include?(:proxy)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def configure_timeouts(http_request)
|
|
102
|
+
http_request.open_timeout = @globals[:open_timeout] if @globals.include?(:open_timeout)
|
|
103
|
+
http_request.read_timeout = @globals[:read_timeout] if @globals.include?(:read_timeout)
|
|
104
|
+
http_request.write_timeout = @globals[:write_timeout] if @globals.include?(:write_timeout)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Configures SSL on the HTTPI::Request from all ssl globals.
|
|
108
|
+
# SSL option reference: https://github.com/savonrb/httpi/blob/main/lib/httpi/auth/ssl.rb
|
|
109
|
+
def configure_ssl(http_request)
|
|
110
|
+
ssl = http_request.auth.ssl
|
|
111
|
+
ssl.ssl_version = @globals[:ssl_version] if @globals.include?(:ssl_version)
|
|
112
|
+
ssl.min_version = @globals[:ssl_min_version] if @globals.include?(:ssl_min_version)
|
|
113
|
+
ssl.max_version = @globals[:ssl_max_version] if @globals.include?(:ssl_max_version)
|
|
114
|
+
ssl.verify_mode = @globals[:ssl_verify_mode] if @globals.include?(:ssl_verify_mode)
|
|
115
|
+
ssl.ciphers = @globals[:ssl_ciphers] if @globals.include?(:ssl_ciphers)
|
|
116
|
+
ssl.cert_key_file = @globals[:ssl_cert_key_file] if @globals.include?(:ssl_cert_key_file)
|
|
117
|
+
ssl.cert_key = @globals[:ssl_cert_key] if @globals.include?(:ssl_cert_key)
|
|
118
|
+
ssl.cert_file = @globals[:ssl_cert_file] if @globals.include?(:ssl_cert_file)
|
|
119
|
+
ssl.cert = @globals[:ssl_cert] if @globals.include?(:ssl_cert)
|
|
120
|
+
ssl.ca_cert_file = @globals[:ssl_ca_cert_file] if @globals.include?(:ssl_ca_cert_file)
|
|
121
|
+
ssl.ca_cert_path = @globals[:ssl_ca_cert_path] if @globals.include?(:ssl_ca_cert_path)
|
|
122
|
+
ssl.ca_cert = @globals[:ssl_ca_cert] if @globals.include?(:ssl_ca_cert)
|
|
123
|
+
ssl.cert_store = @globals[:ssl_cert_store] if @globals.include?(:ssl_cert_store)
|
|
124
|
+
ssl.cert_key_password = @globals[:ssl_cert_key_password] if @globals.include?(:ssl_cert_key_password)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def configure_auth(http_request)
|
|
128
|
+
http_request.auth.basic(*@globals[:basic_auth]) if @globals.include?(:basic_auth)
|
|
129
|
+
http_request.auth.digest(*@globals[:digest_auth]) if @globals.include?(:digest_auth)
|
|
130
|
+
http_request.auth.ntlm(*@globals[:ntlm]) if @globals.include?(:ntlm)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def configure_redirect_handling(http_request)
|
|
134
|
+
http_request.follow_redirect = @globals[:follow_redirects] if @globals.include?(:follow_redirects)
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
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,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "httpi"
|
|
4
|
+
require "savon/transport/faraday"
|
|
5
|
+
|
|
6
|
+
module Savon
|
|
7
|
+
module Transport
|
|
8
|
+
# Transport-agnostic HTTP response value object.
|
|
9
|
+
#
|
|
10
|
+
# Every transport produces a Transport::Response so that higher-level code
|
|
11
|
+
# never depends on transport-specific code. Immutable once constructed.
|
|
12
|
+
#
|
|
13
|
+
# The shape of #cookies is transport-specific: HTTPI responses expose an
|
|
14
|
+
# Array of HTTPI::Cookie, while Faraday responses expose a plain Hash so
|
|
15
|
+
# Faraday users do not depend on HTTPI types.
|
|
16
|
+
class Response
|
|
17
|
+
# Builds a Response from an HTTPI::Response.
|
|
18
|
+
#
|
|
19
|
+
# @param httpi_response [HTTPI::Response]
|
|
20
|
+
# @return [Transport::Response]
|
|
21
|
+
def self.from_httpi(httpi_response)
|
|
22
|
+
new(
|
|
23
|
+
httpi_response.code,
|
|
24
|
+
httpi_response.headers,
|
|
25
|
+
httpi_response.body,
|
|
26
|
+
cookies: ::HTTPI::Cookie.list_from_headers(httpi_response.headers)
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Builds a Response from a Faraday::Response.
|
|
31
|
+
#
|
|
32
|
+
# @param faraday_response [Faraday::Response]
|
|
33
|
+
# @return [Transport::Response]
|
|
34
|
+
def self.from_faraday(faraday_response)
|
|
35
|
+
new(
|
|
36
|
+
faraday_response.status,
|
|
37
|
+
faraday_response.headers.to_h,
|
|
38
|
+
faraday_response.body,
|
|
39
|
+
cookies: Savon::Transport::Faraday.parse_cookies(faraday_response.headers)
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# @param code [Integer] HTTP status code
|
|
44
|
+
# @param headers [Hash] response headers
|
|
45
|
+
# @param body [String] response body
|
|
46
|
+
# @param cookies [Object] parsed cookies in a transport-specific shape
|
|
47
|
+
def initialize(code, headers, body, cookies: nil)
|
|
48
|
+
@code = code
|
|
49
|
+
@headers = headers
|
|
50
|
+
@body = body
|
|
51
|
+
@cookies = cookies
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Returns the HTTP status code.
|
|
55
|
+
attr_reader :code
|
|
56
|
+
|
|
57
|
+
# Returns the response headers hash.
|
|
58
|
+
attr_reader :headers
|
|
59
|
+
|
|
60
|
+
# Returns the response body string.
|
|
61
|
+
attr_reader :body
|
|
62
|
+
|
|
63
|
+
# Returns the parsed cookies in a transport-specific shape.
|
|
64
|
+
# See class-level docs.
|
|
65
|
+
attr_reader :cookies
|
|
66
|
+
|
|
67
|
+
# Returns true when the HTTP status code indicates an error (>= 300).
|
|
68
|
+
def error?
|
|
69
|
+
@code >= 300
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
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"
|