savon 1.2.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|