savon 1.2.0 → 2.0.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.
- data/CHANGELOG.md +119 -104
- data/README.md +12 -11
- data/Rakefile +0 -6
- data/lib/savon.rb +16 -14
- data/lib/savon/block_interface.rb +26 -0
- data/lib/savon/builder.rb +142 -0
- data/lib/savon/client.rb +36 -135
- data/lib/savon/header.rb +42 -0
- data/lib/savon/http_error.rb +27 -0
- data/lib/savon/log_message.rb +23 -25
- data/lib/savon/message.rb +35 -0
- data/lib/savon/mock.rb +5 -0
- data/lib/savon/mock/expectation.rb +70 -0
- data/lib/savon/mock/spec_helper.rb +62 -0
- data/lib/savon/model.rb +39 -61
- data/lib/savon/operation.rb +62 -0
- data/lib/savon/options.rb +265 -0
- data/lib/savon/qualified_message.rb +49 -0
- data/lib/savon/request.rb +92 -0
- data/lib/savon/response.rb +97 -0
- data/lib/savon/soap_fault.rb +40 -0
- data/lib/savon/version.rb +1 -1
- data/savon.gemspec +10 -8
- data/spec/integration/options_spec.rb +536 -0
- data/spec/integration/request_spec.rb +31 -16
- data/spec/integration/support/application.rb +80 -0
- data/spec/integration/support/server.rb +84 -0
- data/spec/savon/builder_spec.rb +81 -0
- data/spec/savon/client_spec.rb +90 -488
- data/spec/savon/http_error_spec.rb +49 -0
- data/spec/savon/log_message_spec.rb +33 -0
- data/spec/savon/mock_spec.rb +127 -0
- data/spec/savon/model_spec.rb +110 -99
- data/spec/savon/observers_spec.rb +92 -0
- data/spec/savon/operation_spec.rb +49 -0
- data/spec/savon/request_spec.rb +145 -0
- data/spec/savon/{soap/response_spec.rb → response_spec.rb} +22 -59
- data/spec/savon/soap_fault_spec.rb +94 -0
- data/spec/spec_helper.rb +5 -3
- data/spec/support/fixture.rb +5 -1
- metadata +202 -197
- data/lib/savon/config.rb +0 -46
- data/lib/savon/error.rb +0 -6
- data/lib/savon/hooks/group.rb +0 -68
- data/lib/savon/hooks/hook.rb +0 -61
- data/lib/savon/http/error.rb +0 -42
- data/lib/savon/logger.rb +0 -39
- data/lib/savon/null_logger.rb +0 -10
- data/lib/savon/soap.rb +0 -21
- data/lib/savon/soap/fault.rb +0 -59
- data/lib/savon/soap/invalid_response_error.rb +0 -13
- data/lib/savon/soap/request.rb +0 -86
- data/lib/savon/soap/request_builder.rb +0 -205
- data/lib/savon/soap/response.rb +0 -117
- data/lib/savon/soap/xml.rb +0 -257
- data/spec/savon/config_spec.rb +0 -38
- data/spec/savon/hooks/group_spec.rb +0 -71
- data/spec/savon/hooks/hook_spec.rb +0 -16
- data/spec/savon/http/error_spec.rb +0 -52
- data/spec/savon/logger_spec.rb +0 -51
- data/spec/savon/savon_spec.rb +0 -33
- data/spec/savon/soap/fault_spec.rb +0 -89
- data/spec/savon/soap/request_builder_spec.rb +0 -207
- data/spec/savon/soap/request_spec.rb +0 -112
- data/spec/savon/soap/xml_spec.rb +0 -357
- data/spec/savon/soap_spec.rb +0 -16
@@ -0,0 +1,62 @@
|
|
1
|
+
require "savon/options"
|
2
|
+
require "savon/block_interface"
|
3
|
+
require "savon/request"
|
4
|
+
require "savon/builder"
|
5
|
+
|
6
|
+
module Savon
|
7
|
+
class Operation
|
8
|
+
|
9
|
+
def self.create(operation_name, wsdl, globals)
|
10
|
+
if wsdl.document?
|
11
|
+
ensure_name_is_symbol! operation_name
|
12
|
+
ensure_exists! operation_name, wsdl
|
13
|
+
end
|
14
|
+
|
15
|
+
new(operation_name, wsdl, globals)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.ensure_exists!(operation_name, wsdl)
|
19
|
+
unless wsdl.soap_actions.include? operation_name
|
20
|
+
raise ArgumentError, "Unable to find SOAP operation: #{operation_name.inspect}\n" \
|
21
|
+
"Operations provided by your service: #{wsdl.soap_actions.inspect}"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.ensure_name_is_symbol!(operation_name)
|
26
|
+
unless operation_name.kind_of? Symbol
|
27
|
+
raise ArgumentError, "Expected the first parameter (the name of the operation to call) to be a symbol\n" \
|
28
|
+
"Actual: #{operation_name.inspect} (#{operation_name.class})"
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def initialize(name, wsdl, globals)
|
33
|
+
@name = name
|
34
|
+
@wsdl = wsdl
|
35
|
+
@globals = globals
|
36
|
+
end
|
37
|
+
|
38
|
+
def call(locals = {}, &block)
|
39
|
+
@locals = LocalOptions.new(locals)
|
40
|
+
|
41
|
+
BlockInterface.new(@locals).evaluate(block) if block
|
42
|
+
|
43
|
+
builder = Builder.new(@name, @wsdl, @globals, @locals)
|
44
|
+
request = Request.new(@name, @wsdl, @globals, @locals)
|
45
|
+
|
46
|
+
response = Savon.notify_observers(@name, builder, @globals, @locals)
|
47
|
+
response ||= request.call(builder.to_s)
|
48
|
+
|
49
|
+
raise_expected_httpi_response! unless response.kind_of?(HTTPI::Response)
|
50
|
+
|
51
|
+
Response.new(response, @globals, @locals)
|
52
|
+
end
|
53
|
+
|
54
|
+
private
|
55
|
+
|
56
|
+
def raise_expected_httpi_response!
|
57
|
+
raise Error, "Observers need to return an HTTPI::Response to mock " \
|
58
|
+
"the request or nil to execute the request."
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,265 @@
|
|
1
|
+
require "logger"
|
2
|
+
require "httpi"
|
3
|
+
|
4
|
+
module Savon
|
5
|
+
class Options
|
6
|
+
|
7
|
+
def initialize(options = {})
|
8
|
+
@options = {}
|
9
|
+
assign options
|
10
|
+
end
|
11
|
+
|
12
|
+
def [](option)
|
13
|
+
@options[option]
|
14
|
+
end
|
15
|
+
|
16
|
+
def []=(option, value)
|
17
|
+
value = [value].flatten
|
18
|
+
self.send(option, *value)
|
19
|
+
end
|
20
|
+
|
21
|
+
def include?(option)
|
22
|
+
@options.key? option
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def assign(options)
|
28
|
+
options.each do |option, value|
|
29
|
+
self.send(option, value)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
class GlobalOptions < Options
|
36
|
+
|
37
|
+
def initialize(options = {})
|
38
|
+
defaults = {
|
39
|
+
:encoding => "UTF-8",
|
40
|
+
:soap_version => 1,
|
41
|
+
:logger => Logger.new($stdout),
|
42
|
+
:filters => [],
|
43
|
+
:pretty_print_xml => false,
|
44
|
+
:raise_errors => true,
|
45
|
+
:strip_namespaces => true,
|
46
|
+
:convert_response_tags_to => lambda { |tag| tag.snakecase.to_sym }
|
47
|
+
}
|
48
|
+
|
49
|
+
options = defaults.merge(options)
|
50
|
+
|
51
|
+
# these options are shortcuts on the logger which needs to be set
|
52
|
+
# before it can be modified to set these options.
|
53
|
+
delayed_log = options.delete(:log)
|
54
|
+
delayed_level = options.delete(:log_level)
|
55
|
+
|
56
|
+
super(options)
|
57
|
+
|
58
|
+
log(delayed_log) unless delayed_log.nil?
|
59
|
+
log_level(delayed_level) unless delayed_level.nil?
|
60
|
+
end
|
61
|
+
|
62
|
+
# Location of the local or remote WSDL document.
|
63
|
+
def wsdl(wsdl_address)
|
64
|
+
@options[:wsdl] = wsdl_address
|
65
|
+
end
|
66
|
+
|
67
|
+
# SOAP endpoint.
|
68
|
+
def endpoint(endpoint)
|
69
|
+
@options[:endpoint] = endpoint
|
70
|
+
end
|
71
|
+
|
72
|
+
# Target namespace.
|
73
|
+
def namespace(namespace)
|
74
|
+
@options[:namespace] = namespace
|
75
|
+
end
|
76
|
+
|
77
|
+
# The namespace identifer.
|
78
|
+
def namespace_identifier(identifier)
|
79
|
+
@options[:namespace_identifier] = identifier
|
80
|
+
end
|
81
|
+
|
82
|
+
# Proxy server to use for all requests.
|
83
|
+
def proxy(proxy)
|
84
|
+
@options[:proxy] = proxy
|
85
|
+
end
|
86
|
+
|
87
|
+
# A Hash of HTTP headers.
|
88
|
+
def headers(headers)
|
89
|
+
@options[:headers] = headers
|
90
|
+
end
|
91
|
+
|
92
|
+
# Open timeout in seconds.
|
93
|
+
def open_timeout(open_timeout)
|
94
|
+
@options[:open_timeout] = open_timeout
|
95
|
+
end
|
96
|
+
|
97
|
+
# Read timeout in seconds.
|
98
|
+
def read_timeout(read_timeout)
|
99
|
+
@options[:read_timeout] = read_timeout
|
100
|
+
end
|
101
|
+
|
102
|
+
# The encoding to use. Defaults to "UTF-8".
|
103
|
+
def encoding(encoding)
|
104
|
+
@options[:encoding] = encoding
|
105
|
+
end
|
106
|
+
|
107
|
+
# The global SOAP header. Expected to be a Hash.
|
108
|
+
def soap_header(header)
|
109
|
+
@options[:soap_header] = header
|
110
|
+
end
|
111
|
+
|
112
|
+
# Sets whether elements should be :qualified or unqualified.
|
113
|
+
# If you need to use this option, please open an issue and make
|
114
|
+
# sure to add your WSDL document for debugging.
|
115
|
+
def element_form_default(element_form_default)
|
116
|
+
@options[:element_form_default] = element_form_default
|
117
|
+
end
|
118
|
+
|
119
|
+
# Can be used to change the SOAP envelope namespace identifier.
|
120
|
+
# If you need to use this option, please open an issue and make
|
121
|
+
# sure to add your WSDL document for debugging.
|
122
|
+
def env_namespace(env_namespace)
|
123
|
+
@options[:env_namespace] = env_namespace
|
124
|
+
end
|
125
|
+
|
126
|
+
# Changes the SOAP version to 1 or 2.
|
127
|
+
def soap_version(soap_version)
|
128
|
+
@options[:soap_version] = soap_version
|
129
|
+
end
|
130
|
+
|
131
|
+
# Whether or not to raise SOAP fault and HTTP errors.
|
132
|
+
def raise_errors(raise_errors)
|
133
|
+
@options[:raise_errors] = raise_errors
|
134
|
+
end
|
135
|
+
|
136
|
+
# Whether or not to log.
|
137
|
+
def log(log)
|
138
|
+
if log
|
139
|
+
HTTPI.log = true
|
140
|
+
target = $stdout
|
141
|
+
else
|
142
|
+
HTTPI.log = false
|
143
|
+
windows = RUBY_PLATFORM =~ /(mingw|bccwin|wince|mswin32)/i
|
144
|
+
target = windows ? "NUL:" : "/dev/null"
|
145
|
+
end
|
146
|
+
|
147
|
+
@options[:logger] = Logger.new(target)
|
148
|
+
end
|
149
|
+
|
150
|
+
# The logger to use. Defaults to a Savon::Logger instance.
|
151
|
+
def logger(logger)
|
152
|
+
@options[:logger] = logger
|
153
|
+
end
|
154
|
+
|
155
|
+
# Changes the Logger's log level.
|
156
|
+
def log_level(level)
|
157
|
+
levels = { :debug => 0, :info => 1, :warn => 2, :error => 3, :fatal => 4 }
|
158
|
+
|
159
|
+
unless levels.include? level
|
160
|
+
raise ArgumentError, "Invalid log level: #{level.inspect}\n" \
|
161
|
+
"Expected one of: #{levels.keys.inspect}"
|
162
|
+
end
|
163
|
+
|
164
|
+
@options[:logger].level = levels[level]
|
165
|
+
end
|
166
|
+
|
167
|
+
# A list of XML tags to filter from logged SOAP messages.
|
168
|
+
def filters(*filters)
|
169
|
+
@options[:filters] = filters.flatten
|
170
|
+
end
|
171
|
+
|
172
|
+
# Whether to pretty print request and response XML log messages.
|
173
|
+
def pretty_print_xml(pretty_print_xml)
|
174
|
+
@options[:pretty_print_xml] = pretty_print_xml
|
175
|
+
end
|
176
|
+
|
177
|
+
# Used by Savon to store the last response to pass
|
178
|
+
# its cookies to the next request.
|
179
|
+
def last_response(last_response)
|
180
|
+
@options[:last_response] = last_response
|
181
|
+
end
|
182
|
+
|
183
|
+
# HTTP basic auth credentials.
|
184
|
+
def basic_auth(*credentials)
|
185
|
+
@options[:basic_auth] = credentials.flatten
|
186
|
+
end
|
187
|
+
|
188
|
+
# HTTP digest auth credentials.
|
189
|
+
def digest_auth(*credentials)
|
190
|
+
@options[:digest_auth] = credentials.flatten
|
191
|
+
end
|
192
|
+
|
193
|
+
# WSSE auth credentials for Akami.
|
194
|
+
def wsse_auth(*credentials)
|
195
|
+
@options[:wsse_auth] = credentials.flatten
|
196
|
+
end
|
197
|
+
|
198
|
+
# Instruct Akami to enable wsu:Timestamp headers.
|
199
|
+
def wsse_timestamp(*timestamp)
|
200
|
+
@options[:wsse_timestamp] = timestamp.flatten
|
201
|
+
end
|
202
|
+
|
203
|
+
# Instruct Nori whether to strip namespaces from XML nodes.
|
204
|
+
def strip_namespaces(strip_namespaces)
|
205
|
+
@options[:strip_namespaces] = strip_namespaces
|
206
|
+
end
|
207
|
+
|
208
|
+
# Tell Gyoku how to convert Hash key Symbols to XML tags.
|
209
|
+
# Accepts one of :lower_camelcase, :camelcase, :upcase, or :none.
|
210
|
+
def convert_request_keys_to(converter)
|
211
|
+
@options[:convert_request_keys_to] = converter
|
212
|
+
end
|
213
|
+
|
214
|
+
# Tell Nori how to convert XML tags from the SOAP response into Hash keys.
|
215
|
+
# Accepts a lambda or a block which receives an XML tag and returns a Hash key.
|
216
|
+
# Defaults to convert tags to snakecase Symbols.
|
217
|
+
def convert_response_tags_to(converter = nil, &block)
|
218
|
+
@options[:convert_response_tags_to] = block || converter
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
class LocalOptions < Options
|
223
|
+
|
224
|
+
def initialize(options = {})
|
225
|
+
defaults = {
|
226
|
+
:advanced_typecasting => true,
|
227
|
+
:response_parser => :nokogiri
|
228
|
+
}
|
229
|
+
|
230
|
+
super defaults.merge(options)
|
231
|
+
end
|
232
|
+
|
233
|
+
# The SOAP message to send. Expected to be a Hash or a String.
|
234
|
+
def message(message)
|
235
|
+
@options[:message] = message
|
236
|
+
end
|
237
|
+
|
238
|
+
# SOAP message tag (formerly known as SOAP input tag). If it's not set, Savon retrieves the name from
|
239
|
+
# the WSDL document (if available). Otherwise, Gyoku converts the operation name into an XML element.
|
240
|
+
def message_tag(message_tag)
|
241
|
+
@options[:message_tag] = message_tag
|
242
|
+
end
|
243
|
+
|
244
|
+
# Value of the SOAPAction HTTP header.
|
245
|
+
def soap_action(soap_action)
|
246
|
+
@options[:soap_action] = soap_action
|
247
|
+
end
|
248
|
+
|
249
|
+
# The SOAP request XML to send. Expected to be a String.
|
250
|
+
def xml(xml)
|
251
|
+
@options[:xml] = xml
|
252
|
+
end
|
253
|
+
|
254
|
+
# Instruct Nori to use advanced typecasting.
|
255
|
+
def advanced_typecasting(advanced)
|
256
|
+
@options[:advanced_typecasting] = advanced
|
257
|
+
end
|
258
|
+
|
259
|
+
# Instruct Nori to use :rexml or :nokogiri to parse the response.
|
260
|
+
def response_parser(parser)
|
261
|
+
@options[:response_parser] = parser
|
262
|
+
end
|
263
|
+
|
264
|
+
end
|
265
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "gyoku"
|
2
|
+
|
3
|
+
module Savon
|
4
|
+
class QualifiedMessage
|
5
|
+
|
6
|
+
def initialize(types, used_namespaces, key_converter)
|
7
|
+
@types = types
|
8
|
+
@used_namespaces = used_namespaces
|
9
|
+
@key_converter = key_converter
|
10
|
+
end
|
11
|
+
|
12
|
+
def to_hash(hash, path)
|
13
|
+
return unless hash
|
14
|
+
return hash.map { |value| add_namespaces_to_body(value, path) } if hash.kind_of?(Array)
|
15
|
+
return hash.to_s unless hash.kind_of? Hash
|
16
|
+
|
17
|
+
hash.inject({}) do |newhash, (key, value)|
|
18
|
+
camelcased_key = Gyoku.xml_tag(key, :key_converter => @key_converter).to_s
|
19
|
+
newpath = path + [camelcased_key]
|
20
|
+
|
21
|
+
if @used_namespaces[newpath]
|
22
|
+
newhash.merge(
|
23
|
+
"#{@used_namespaces[newpath]}:#{camelcased_key}" =>
|
24
|
+
add_namespaces_to_body(value, @types[newpath] ? [@types[newpath]] : newpath)
|
25
|
+
)
|
26
|
+
else
|
27
|
+
add_namespaces_to_values(value, path) if key == :order!
|
28
|
+
newhash.merge(key => value)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def add_namespaces_to_body(value, path)
|
36
|
+
QualifiedMessage.new(@types, @used_namespaces, @key_converter).to_hash(value, path)
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_namespaces_to_values(values, path)
|
40
|
+
values.collect! { |value|
|
41
|
+
camelcased_value = Gyoku.xml_tag(value, :key_converter => @key_converter)
|
42
|
+
namespace_path = path + [camelcased_value.to_s]
|
43
|
+
namespace = @used_namespaces[namespace_path]
|
44
|
+
"#{namespace.blank? ? '' : namespace + ":"}#{camelcased_value}"
|
45
|
+
}
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
require "httpi"
|
2
|
+
require "savon/response"
|
3
|
+
require "savon/log_message"
|
4
|
+
|
5
|
+
module Savon
|
6
|
+
class Request
|
7
|
+
|
8
|
+
CONTENT_TYPE = {
|
9
|
+
1 => "text/xml;charset=%s",
|
10
|
+
2 => "application/soap+xml;charset=%s"
|
11
|
+
}
|
12
|
+
|
13
|
+
def initialize(operation_name, wsdl, globals, locals)
|
14
|
+
@operation_name = operation_name
|
15
|
+
|
16
|
+
@wsdl = wsdl
|
17
|
+
@globals = globals
|
18
|
+
@locals = locals
|
19
|
+
@http = create_http_client
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_reader :http
|
23
|
+
|
24
|
+
def call(xml)
|
25
|
+
@http.body = xml
|
26
|
+
@http.headers["Content-Length"] = xml.bytesize.to_s
|
27
|
+
|
28
|
+
log_request @http.url, @http.headers, @http.body
|
29
|
+
response = HTTPI.post(@http)
|
30
|
+
log_response response.code, response.body
|
31
|
+
|
32
|
+
response
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def create_http_client
|
38
|
+
http = HTTPI::Request.new
|
39
|
+
http.url = @globals[:endpoint] || @wsdl.endpoint
|
40
|
+
|
41
|
+
http.proxy = @globals[:proxy] if @globals.include? :proxy
|
42
|
+
http.set_cookies @globals[:last_response] if @globals.include? :last_response
|
43
|
+
|
44
|
+
http.open_timeout = @globals[:open_timeout] if @globals.include? :open_timeout
|
45
|
+
http.read_timeout = @globals[:read_timeout] if @globals.include? :read_timeout
|
46
|
+
|
47
|
+
http.headers = @globals[:headers] if @globals.include? :headers
|
48
|
+
http.headers["SOAPAction"] ||= %{"#{soap_action}"} if soap_action
|
49
|
+
http.headers["Content-Type"] = CONTENT_TYPE[@globals[:soap_version]] % @globals[:encoding]
|
50
|
+
|
51
|
+
http.auth.basic(*@globals[:basic_auth]) if @globals.include? :basic_auth
|
52
|
+
http.auth.digest(*@globals[:digest_auth]) if @globals.include? :digest_auth
|
53
|
+
|
54
|
+
http
|
55
|
+
end
|
56
|
+
|
57
|
+
def soap_action
|
58
|
+
return if @locals.include?(:soap_action) && !@locals[:soap_action]
|
59
|
+
return @soap_action if defined? @soap_action
|
60
|
+
|
61
|
+
soap_action = @locals[:soap_action]
|
62
|
+
soap_action ||= @wsdl.soap_action(@operation_name.to_sym) if @wsdl.document?
|
63
|
+
soap_action ||= Gyoku.xml_tag(@operation_name, :key_converter => @globals[:convert_request_keys_to])
|
64
|
+
|
65
|
+
@soap_action = soap_action
|
66
|
+
end
|
67
|
+
|
68
|
+
def log_request(url, headers, body)
|
69
|
+
logger.info "SOAP request: #{url}"
|
70
|
+
logger.info headers_to_log(headers)
|
71
|
+
logger.debug body_to_log(body)
|
72
|
+
end
|
73
|
+
|
74
|
+
def log_response(code, body)
|
75
|
+
logger.info "SOAP response (status #{code})"
|
76
|
+
logger.debug body_to_log(body)
|
77
|
+
end
|
78
|
+
|
79
|
+
def headers_to_log(headers)
|
80
|
+
headers.map { |key, value| "#{key}: #{value}" }.join(", ")
|
81
|
+
end
|
82
|
+
|
83
|
+
def body_to_log(body)
|
84
|
+
LogMessage.new(body, @globals[:filters], @globals[:pretty_print_xml]).to_s
|
85
|
+
end
|
86
|
+
|
87
|
+
def logger
|
88
|
+
@globals[:logger]
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
end
|