savon 2.17.3 → 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 -99
- data/README.md +52 -52
- data/Rakefile +2 -6
- data/lib/savon/block_interface.rb +4 -3
- data/lib/savon/builder.rb +33 -34
- data/lib/savon/client.rb +13 -44
- data/lib/savon/header.rb +12 -12
- 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 +82 -104
- 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 -69
- 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
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "faraday"
|
|
3
|
+
|
|
4
|
+
module Savon
|
|
5
|
+
class HTTPRequest
|
|
6
|
+
|
|
7
|
+
def initialize(globals, connection = nil)
|
|
8
|
+
@globals = globals
|
|
9
|
+
@connection = connection || Faraday::Connection.new
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def configure_proxy
|
|
15
|
+
connection.proxy = @globals[:proxy] if @globals.include? :proxy
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configure_timeouts
|
|
19
|
+
connection.options.open_timeout = @globals[:open_timeout] if @globals.include? :open_timeout
|
|
20
|
+
connection.options.read_timeout = @globals[:read_timeout] if @globals.include? :read_timeout
|
|
21
|
+
connection.options.write_timeout = @globals[:write_timeout] if @globals.include? :write_timeout
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def configure_ssl
|
|
25
|
+
connection.ssl.verify = @globals[:ssl_verify] if @globals.include? :ssl_verify
|
|
26
|
+
connection.ssl.ca_file = @globals[:ssl_ca_cert_file] if @globals.include? :ssl_ca_cert_file
|
|
27
|
+
connection.ssl.verify_hostname = @globals[:verify_hostname] if @globals.include? :verify_hostname
|
|
28
|
+
connection.ssl.ca_path = @globals[:ssl_ca_cert_path] if @globals.include? :ssl_ca_cert_path
|
|
29
|
+
connection.ssl.verify_mode = @globals[:ssl_verify_mode] if @globals.include? :ssl_verify_mode
|
|
30
|
+
connection.ssl.cert_store = @globals[:ssl_cert_store] if @globals.include? :ssl_cert_store
|
|
31
|
+
connection.ssl.client_cert = @globals[:ssl_cert] if @globals.include? :ssl_cert
|
|
32
|
+
connection.ssl.client_key = @globals[:ssl_cert_key] if @globals.include? :ssl_cert_key
|
|
33
|
+
connection.ssl.certificate = @globals[:ssl_certificate] if @globals.include? :ssl_certificate
|
|
34
|
+
connection.ssl.private_key = @globals[:ssl_private_key] if @globals.include? :ssl_private_key
|
|
35
|
+
connection.ssl.verify_depth = @globals[:verify_depth] if @globals.include? :verify_depth
|
|
36
|
+
connection.ssl.version = @globals[:ssl_version] if @globals.include? :ssl_version
|
|
37
|
+
connection.ssl.min_version = @globals[:ssl_min_version] if @globals.include? :ssl_min_version
|
|
38
|
+
connection.ssl.max_version = @globals[:ssl_max_version] if @globals.include? :ssl_max_version
|
|
39
|
+
|
|
40
|
+
# No Faraday Equivalent out of box, see: https://lostisland.github.io/faraday/#/customization/ssl-options
|
|
41
|
+
# connection.ssl.cert_file = @globals[:ssl_cert_file] if @globals.include? :ssl_cert_file
|
|
42
|
+
# connection.ssl.cert_key_file = @globals[:ssl_cert_key_file] if @globals.include? :ssl_cert_key_file
|
|
43
|
+
# connection.ssl.ca_cert = @globals[:ssl_ca_cert] if @globals.include? :ssl_ca_cert
|
|
44
|
+
# connection.ssl.ciphers = @globals[:ssl_ciphers] if @globals.include? :ssl_ciphers
|
|
45
|
+
# connection.ssl.cert_key_password = @globals[:ssl_cert_key_password] if @globals.include? :ssl_cert_key_password
|
|
46
|
+
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def configure_auth
|
|
50
|
+
basic_auth if @globals.include?(:basic_auth)
|
|
51
|
+
ntlm_auth if @globals.include?(:ntlm)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def basic_auth
|
|
55
|
+
connection.request(:authorization, :basic, *@globals[:basic_auth])
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def ntlm_auth
|
|
59
|
+
begin
|
|
60
|
+
require 'rubyntlm'
|
|
61
|
+
require 'faraday/net_http_persistent'
|
|
62
|
+
connection.adapter :net_http_persistent, pool_size: 5
|
|
63
|
+
rescue LoadError
|
|
64
|
+
raise LoadError, 'Using NTLM Auth requires both `rubyntlm` and `faraday-net_http_persistent` to be installed.'
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def configure_redirect_handling
|
|
69
|
+
if @globals[:follow_redirects]
|
|
70
|
+
require 'faraday/follow_redirects'
|
|
71
|
+
connection.response :follow_redirects
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def configure_adapter
|
|
76
|
+
connection.adapter(*@globals[:adapter]) unless @globals[:adapter].nil?
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def configure_logging
|
|
80
|
+
connection.response(:logger, nil, headers: @globals[:log_headers], level: @globals[:logger].level) if @globals[:log]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
protected
|
|
84
|
+
attr_reader :connection
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
class WSDLRequest < HTTPRequest
|
|
88
|
+
|
|
89
|
+
def build
|
|
90
|
+
configure_proxy
|
|
91
|
+
configure_timeouts
|
|
92
|
+
configure_ssl
|
|
93
|
+
configure_auth
|
|
94
|
+
configure_adapter
|
|
95
|
+
configure_logging
|
|
96
|
+
configure_headers
|
|
97
|
+
connection
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private
|
|
101
|
+
|
|
102
|
+
def configure_headers
|
|
103
|
+
connection.headers = @globals[:headers] if @globals.include? :headers
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
class SOAPRequest < HTTPRequest
|
|
108
|
+
|
|
109
|
+
CONTENT_TYPE = {
|
|
110
|
+
1 => "text/xml;charset=%s",
|
|
111
|
+
2 => "application/soap+xml;charset=%s"
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
def build(options = {})
|
|
115
|
+
configure_proxy
|
|
116
|
+
configure_timeouts
|
|
117
|
+
configure_ssl
|
|
118
|
+
configure_auth
|
|
119
|
+
configure_headers(options[:soap_action], options[:headers])
|
|
120
|
+
configure_cookies(options[:cookies])
|
|
121
|
+
configure_adapter
|
|
122
|
+
configure_logging
|
|
123
|
+
configure_redirect_handling
|
|
124
|
+
yield(connection) if block_given?
|
|
125
|
+
connection
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def configure_cookies(cookies)
|
|
131
|
+
connection.headers['Cookie'] = cookies.map do |key, value|
|
|
132
|
+
if value.nil?
|
|
133
|
+
key
|
|
134
|
+
else
|
|
135
|
+
"#{key}=#{value}"
|
|
136
|
+
end
|
|
137
|
+
end.join('; ') if cookies
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def configure_headers(soap_action, headers)
|
|
141
|
+
connection.headers = @globals[:headers] if @globals.include? :headers
|
|
142
|
+
connection.headers.merge!(headers) if headers
|
|
143
|
+
connection.headers["SOAPAction"] ||= %{"#{soap_action}"} if soap_action
|
|
144
|
+
connection.headers["Content-Type"] ||= CONTENT_TYPE[@globals[:soap_version]] % @globals[:encoding]
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
require "savon/log_message"
|
|
3
|
+
|
|
4
|
+
module Savon
|
|
5
|
+
class RequestLogger
|
|
6
|
+
|
|
7
|
+
def initialize(globals)
|
|
8
|
+
@globals = globals
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def log(request, &http_request)
|
|
12
|
+
log_request(request) if log?
|
|
13
|
+
response = http_request.call
|
|
14
|
+
log_response(response) if log?
|
|
15
|
+
|
|
16
|
+
response
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def logger
|
|
20
|
+
@globals[:logger]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def log?
|
|
24
|
+
@globals[:log]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def log_headers?
|
|
28
|
+
@globals[:log_headers]
|
|
29
|
+
end
|
|
30
|
+
def log_request(request)
|
|
31
|
+
return unless log?
|
|
32
|
+
logger.info { "SOAP request: #{request.path}" }
|
|
33
|
+
logger.info { headers_to_log(request.headers) } if log_headers?
|
|
34
|
+
logger.debug { body_to_log(request.body) }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def log_response(response)
|
|
38
|
+
return response unless log?
|
|
39
|
+
logger.info { "SOAP response (status #{response.status})" }
|
|
40
|
+
logger.debug { headers_to_log(response.headers) } if log_headers?
|
|
41
|
+
logger.debug { body_to_log(response.body) }
|
|
42
|
+
response
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def headers_to_log(headers)
|
|
49
|
+
headers.map { |key, value| "#{key}: #{value}" }.join("\n")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def body_to_log(body)
|
|
53
|
+
LogMessage.new(body, @globals[:filters], @globals[:pretty_print_xml]).to_s
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
end
|
|
57
|
+
end
|
data/lib/savon/response.rb
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
2
|
require "nori"
|
|
4
3
|
require "savon/soap_fault"
|
|
5
4
|
require "savon/http_error"
|
|
@@ -7,37 +6,33 @@ require "savon/http_error"
|
|
|
7
6
|
module Savon
|
|
8
7
|
class Response
|
|
9
8
|
CRLF = /\r\n/
|
|
10
|
-
WSP = /[\
|
|
9
|
+
WSP = /[#{%Q|\x9\x20|}]/
|
|
11
10
|
|
|
12
|
-
def initialize(
|
|
13
|
-
@
|
|
14
|
-
@globals
|
|
15
|
-
@locals
|
|
16
|
-
@attachments
|
|
17
|
-
@xml
|
|
18
|
-
@has_parsed_body
|
|
11
|
+
def initialize(http, globals, locals)
|
|
12
|
+
@http = http
|
|
13
|
+
@globals = globals
|
|
14
|
+
@locals = locals
|
|
15
|
+
@attachments = []
|
|
16
|
+
@xml = ''
|
|
17
|
+
@has_parsed_body = false
|
|
19
18
|
|
|
20
19
|
build_soap_and_http_errors!
|
|
21
20
|
raise_soap_and_http_errors! if @globals[:raise_errors]
|
|
22
21
|
end
|
|
23
22
|
|
|
24
|
-
attr_reader :globals, :locals, :soap_fault, :http_error
|
|
25
|
-
|
|
26
|
-
def http
|
|
27
|
-
@transport_response
|
|
28
|
-
end
|
|
23
|
+
attr_reader :http, :globals, :locals, :soap_fault, :http_error
|
|
29
24
|
|
|
30
25
|
def success?
|
|
31
26
|
!soap_fault? && !http_error?
|
|
32
27
|
end
|
|
33
|
-
|
|
28
|
+
alias_method :successful?, :success?
|
|
34
29
|
|
|
35
30
|
def soap_fault?
|
|
36
|
-
SOAPFault.present?(http, xml)
|
|
31
|
+
SOAPFault.present?(@http, xml)
|
|
37
32
|
end
|
|
38
33
|
|
|
39
34
|
def http_error?
|
|
40
|
-
HTTPError.present? http
|
|
35
|
+
HTTPError.present? @http
|
|
41
36
|
end
|
|
42
37
|
|
|
43
38
|
def header
|
|
@@ -48,21 +43,15 @@ module Savon
|
|
|
48
43
|
find('Body')
|
|
49
44
|
end
|
|
50
45
|
|
|
51
|
-
|
|
46
|
+
alias_method :to_hash, :body
|
|
52
47
|
|
|
53
48
|
def to_array(*path)
|
|
54
|
-
result = path.inject
|
|
49
|
+
result = path.inject body do |memo, key|
|
|
55
50
|
return [] if memo[key].nil?
|
|
56
|
-
|
|
57
51
|
memo[key]
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
result.is_a?(Array) ? result.compact : [result].compact
|
|
61
|
-
end
|
|
52
|
+
end
|
|
62
53
|
|
|
63
|
-
|
|
64
|
-
warn "Savon::Response#hash is deprecated and will be removed in version 3 - use #full_hash instead", uplevel: 1
|
|
65
|
-
full_hash
|
|
54
|
+
result.kind_of?(Array) ? result.compact : [result].compact
|
|
66
55
|
end
|
|
67
56
|
|
|
68
57
|
def full_hash
|
|
@@ -74,12 +63,12 @@ module Savon
|
|
|
74
63
|
parse_body unless @has_parsed_body
|
|
75
64
|
@xml
|
|
76
65
|
else
|
|
77
|
-
http.body
|
|
66
|
+
@http.body
|
|
78
67
|
end
|
|
79
68
|
end
|
|
80
69
|
|
|
81
|
-
|
|
82
|
-
|
|
70
|
+
alias_method :to_xml, :xml
|
|
71
|
+
alias_method :to_s, :xml
|
|
83
72
|
|
|
84
73
|
def doc
|
|
85
74
|
@doc ||= Nokogiri.XML(xml)
|
|
@@ -113,20 +102,19 @@ module Savon
|
|
|
113
102
|
|
|
114
103
|
def boundary
|
|
115
104
|
return unless multipart?
|
|
116
|
-
|
|
117
105
|
Mail::Field.new('content-type', http.headers['content-type']).parameters['boundary']
|
|
118
106
|
end
|
|
119
107
|
|
|
120
108
|
def parse_body
|
|
121
109
|
http.body.force_encoding Encoding::ASCII_8BIT
|
|
122
110
|
parts = http.body.split(/(?:\A|\r\n)--#{Regexp.escape(boundary)}(?=(?:--)?\s*$)/)
|
|
123
|
-
parts[1
|
|
111
|
+
parts[1..-1].to_a.each_with_index do |part, index|
|
|
124
112
|
header_part, body_part = part.lstrip.split(/#{CRLF}#{CRLF}|#{CRLF}#{WSP}*#{CRLF}(?!#{WSP})/m, 2)
|
|
125
113
|
section = Mail::Part.new(
|
|
126
114
|
body: body_part
|
|
127
115
|
)
|
|
128
116
|
section.header = header_part
|
|
129
|
-
if index
|
|
117
|
+
if index == 0
|
|
130
118
|
@xml = section.body.to_s
|
|
131
119
|
else
|
|
132
120
|
@attachments << section
|
|
@@ -136,8 +124,8 @@ module Savon
|
|
|
136
124
|
end
|
|
137
125
|
|
|
138
126
|
def build_soap_and_http_errors!
|
|
139
|
-
@soap_fault = SOAPFault.new(http, nori, xml) if soap_fault?
|
|
140
|
-
@http_error = HTTPError.new(http) if http_error?
|
|
127
|
+
@soap_fault = SOAPFault.new(@http, nori, xml) if soap_fault?
|
|
128
|
+
@http_error = HTTPError.new(@http) if http_error?
|
|
141
129
|
end
|
|
142
130
|
|
|
143
131
|
def raise_soap_and_http_errors!
|
|
@@ -146,7 +134,7 @@ module Savon
|
|
|
146
134
|
end
|
|
147
135
|
|
|
148
136
|
def raise_invalid_response_error!
|
|
149
|
-
raise InvalidResponseError, "Unable to parse response body:\n
|
|
137
|
+
raise InvalidResponseError, "Unable to parse response body:\n" + xml.inspect
|
|
150
138
|
end
|
|
151
139
|
|
|
152
140
|
def xml_namespaces
|
|
@@ -157,19 +145,17 @@ module Savon
|
|
|
157
145
|
return @nori if @nori
|
|
158
146
|
|
|
159
147
|
nori_options = {
|
|
160
|
-
delete_namespace_attributes
|
|
161
|
-
strip_namespaces
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
convert_attributes_to: @globals[:convert_attributes_to],
|
|
167
|
-
advanced_typecasting: @locals[:advanced_typecasting],
|
|
168
|
-
parser: @locals[:response_parser]
|
|
148
|
+
:delete_namespace_attributes => @globals[:delete_namespace_attributes],
|
|
149
|
+
:strip_namespaces => @globals[:strip_namespaces],
|
|
150
|
+
:convert_tags_to => @globals[:convert_response_tags_to],
|
|
151
|
+
:convert_attributes_to => @globals[:convert_attributes_to],
|
|
152
|
+
:advanced_typecasting => @locals[:advanced_typecasting],
|
|
153
|
+
:parser => @locals[:response_parser]
|
|
169
154
|
}
|
|
170
155
|
|
|
171
156
|
non_nil_nori_options = nori_options.reject { |_, value| value.nil? }
|
|
172
157
|
@nori = Nori.new(non_nil_nori_options)
|
|
173
158
|
end
|
|
159
|
+
|
|
174
160
|
end
|
|
175
161
|
end
|
data/lib/savon/soap_fault.rb
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
2
|
module Savon
|
|
4
3
|
class SOAPFault < Error
|
|
4
|
+
|
|
5
5
|
def self.present?(http, xml = nil)
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
soap2_fault = body.include?("Code>") && body.include?("Reason>")
|
|
6
|
+
xml ||= http.body
|
|
7
|
+
fault_node = xml.include?("Fault>")
|
|
8
|
+
soap1_fault = xml.match(/faultcode\/?>/) && xml.match(/faultstring\/?>/)
|
|
9
|
+
soap2_fault = xml.include?("Code>") && xml.include?("Reason>")
|
|
11
10
|
|
|
12
11
|
fault_node && (soap1_fault || soap2_fault)
|
|
13
12
|
end
|
|
@@ -45,5 +44,6 @@ module Savon
|
|
|
45
44
|
"(#{code}) #{text}"
|
|
46
45
|
end
|
|
47
46
|
end
|
|
47
|
+
|
|
48
48
|
end
|
|
49
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,3 +14,4 @@ module Savon
|
|
|
14
14
|
end
|
|
15
15
|
end
|
|
16
16
|
end
|
|
17
|
+
|
data/lib/savon/version.rb
CHANGED
data/lib/savon.rb
CHANGED
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
|
-
|
|
3
2
|
module Savon
|
|
3
|
+
|
|
4
4
|
Error = Class.new(RuntimeError)
|
|
5
5
|
InitializationError = Class.new(Error)
|
|
6
6
|
UnknownOptionError = Class.new(Error)
|
|
7
7
|
UnknownOperationError = Class.new(Error)
|
|
8
8
|
InvalidResponseError = Class.new(Error)
|
|
9
9
|
|
|
10
|
+
class DeprecatedOptionError < Error
|
|
11
|
+
attr_accessor :option
|
|
12
|
+
def initialize(option)
|
|
13
|
+
@option = option
|
|
14
|
+
super("#{option} is deprecated as it is not supported in Faraday. See https://github.com/savonrb/savon/blob/main/UPGRADING.md for more information.")
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
10
18
|
def self.client(globals = {}, &block)
|
|
11
19
|
Client.new(globals, &block)
|
|
12
20
|
end
|
|
@@ -16,10 +24,11 @@ module Savon
|
|
|
16
24
|
end
|
|
17
25
|
|
|
18
26
|
def self.notify_observers(operation_name, builder, globals, locals)
|
|
19
|
-
observers.inject(nil) do |
|
|
27
|
+
observers.inject(nil) do |response, observer|
|
|
20
28
|
observer.notify(operation_name, builder, globals, locals)
|
|
21
29
|
end
|
|
22
30
|
end
|
|
31
|
+
|
|
23
32
|
end
|
|
24
33
|
|
|
25
34
|
require "savon/version"
|