savon 2.17.2 → 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 -91
- 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 +5 -3
- 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 +74 -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 +23 -33
- 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 -98
- data/lib/savon/transport/httpi.rb +0 -135
- data/lib/savon/transport/logging.rb +0 -60
- data/lib/savon/transport/response.rb +0 -44
|
@@ -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,14 +6,14 @@ 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
11
|
def initialize(http, globals, locals)
|
|
13
|
-
@http
|
|
14
|
-
@globals
|
|
15
|
-
@locals
|
|
16
|
-
@attachments
|
|
17
|
-
@xml
|
|
12
|
+
@http = http
|
|
13
|
+
@globals = globals
|
|
14
|
+
@locals = locals
|
|
15
|
+
@attachments = []
|
|
16
|
+
@xml = ''
|
|
18
17
|
@has_parsed_body = false
|
|
19
18
|
|
|
20
19
|
build_soap_and_http_errors!
|
|
@@ -26,7 +25,7 @@ module Savon
|
|
|
26
25
|
def success?
|
|
27
26
|
!soap_fault? && !http_error?
|
|
28
27
|
end
|
|
29
|
-
|
|
28
|
+
alias_method :successful?, :success?
|
|
30
29
|
|
|
31
30
|
def soap_fault?
|
|
32
31
|
SOAPFault.present?(@http, xml)
|
|
@@ -44,21 +43,15 @@ module Savon
|
|
|
44
43
|
find('Body')
|
|
45
44
|
end
|
|
46
45
|
|
|
47
|
-
|
|
46
|
+
alias_method :to_hash, :body
|
|
48
47
|
|
|
49
48
|
def to_array(*path)
|
|
50
|
-
result = path.inject
|
|
49
|
+
result = path.inject body do |memo, key|
|
|
51
50
|
return [] if memo[key].nil?
|
|
52
|
-
|
|
53
51
|
memo[key]
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
result.is_a?(Array) ? result.compact : [result].compact
|
|
57
|
-
end
|
|
52
|
+
end
|
|
58
53
|
|
|
59
|
-
|
|
60
|
-
warn "Savon::Response#hash is deprecated and will be removed in version 3 - use #full_hash instead", uplevel: 1
|
|
61
|
-
full_hash
|
|
54
|
+
result.kind_of?(Array) ? result.compact : [result].compact
|
|
62
55
|
end
|
|
63
56
|
|
|
64
57
|
def full_hash
|
|
@@ -74,8 +67,8 @@ module Savon
|
|
|
74
67
|
end
|
|
75
68
|
end
|
|
76
69
|
|
|
77
|
-
|
|
78
|
-
|
|
70
|
+
alias_method :to_xml, :xml
|
|
71
|
+
alias_method :to_s, :xml
|
|
79
72
|
|
|
80
73
|
def doc
|
|
81
74
|
@doc ||= Nokogiri.XML(xml)
|
|
@@ -109,20 +102,19 @@ module Savon
|
|
|
109
102
|
|
|
110
103
|
def boundary
|
|
111
104
|
return unless multipart?
|
|
112
|
-
|
|
113
105
|
Mail::Field.new('content-type', http.headers['content-type']).parameters['boundary']
|
|
114
106
|
end
|
|
115
107
|
|
|
116
108
|
def parse_body
|
|
117
109
|
http.body.force_encoding Encoding::ASCII_8BIT
|
|
118
110
|
parts = http.body.split(/(?:\A|\r\n)--#{Regexp.escape(boundary)}(?=(?:--)?\s*$)/)
|
|
119
|
-
parts[1
|
|
111
|
+
parts[1..-1].to_a.each_with_index do |part, index|
|
|
120
112
|
header_part, body_part = part.lstrip.split(/#{CRLF}#{CRLF}|#{CRLF}#{WSP}*#{CRLF}(?!#{WSP})/m, 2)
|
|
121
113
|
section = Mail::Part.new(
|
|
122
114
|
body: body_part
|
|
123
115
|
)
|
|
124
116
|
section.header = header_part
|
|
125
|
-
if index
|
|
117
|
+
if index == 0
|
|
126
118
|
@xml = section.body.to_s
|
|
127
119
|
else
|
|
128
120
|
@attachments << section
|
|
@@ -142,7 +134,7 @@ module Savon
|
|
|
142
134
|
end
|
|
143
135
|
|
|
144
136
|
def raise_invalid_response_error!
|
|
145
|
-
raise InvalidResponseError, "Unable to parse response body:\n
|
|
137
|
+
raise InvalidResponseError, "Unable to parse response body:\n" + xml.inspect
|
|
146
138
|
end
|
|
147
139
|
|
|
148
140
|
def xml_namespaces
|
|
@@ -153,19 +145,17 @@ module Savon
|
|
|
153
145
|
return @nori if @nori
|
|
154
146
|
|
|
155
147
|
nori_options = {
|
|
156
|
-
delete_namespace_attributes
|
|
157
|
-
strip_namespaces
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
convert_attributes_to: @globals[:convert_attributes_to],
|
|
163
|
-
advanced_typecasting: @locals[:advanced_typecasting],
|
|
164
|
-
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]
|
|
165
154
|
}
|
|
166
155
|
|
|
167
156
|
non_nil_nori_options = nori_options.reject { |_, value| value.nil? }
|
|
168
157
|
@nori = Nori.new(non_nil_nori_options)
|
|
169
158
|
end
|
|
159
|
+
|
|
170
160
|
end
|
|
171
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"
|