savon 1.2.0 → 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/CHANGELOG.md +119 -104
  2. data/README.md +12 -11
  3. data/Rakefile +0 -6
  4. data/lib/savon.rb +16 -14
  5. data/lib/savon/block_interface.rb +26 -0
  6. data/lib/savon/builder.rb +142 -0
  7. data/lib/savon/client.rb +36 -135
  8. data/lib/savon/header.rb +42 -0
  9. data/lib/savon/http_error.rb +27 -0
  10. data/lib/savon/log_message.rb +23 -25
  11. data/lib/savon/message.rb +35 -0
  12. data/lib/savon/mock.rb +5 -0
  13. data/lib/savon/mock/expectation.rb +70 -0
  14. data/lib/savon/mock/spec_helper.rb +62 -0
  15. data/lib/savon/model.rb +39 -61
  16. data/lib/savon/operation.rb +62 -0
  17. data/lib/savon/options.rb +265 -0
  18. data/lib/savon/qualified_message.rb +49 -0
  19. data/lib/savon/request.rb +92 -0
  20. data/lib/savon/response.rb +97 -0
  21. data/lib/savon/soap_fault.rb +40 -0
  22. data/lib/savon/version.rb +1 -1
  23. data/savon.gemspec +10 -8
  24. data/spec/integration/options_spec.rb +536 -0
  25. data/spec/integration/request_spec.rb +31 -16
  26. data/spec/integration/support/application.rb +80 -0
  27. data/spec/integration/support/server.rb +84 -0
  28. data/spec/savon/builder_spec.rb +81 -0
  29. data/spec/savon/client_spec.rb +90 -488
  30. data/spec/savon/http_error_spec.rb +49 -0
  31. data/spec/savon/log_message_spec.rb +33 -0
  32. data/spec/savon/mock_spec.rb +127 -0
  33. data/spec/savon/model_spec.rb +110 -99
  34. data/spec/savon/observers_spec.rb +92 -0
  35. data/spec/savon/operation_spec.rb +49 -0
  36. data/spec/savon/request_spec.rb +145 -0
  37. data/spec/savon/{soap/response_spec.rb → response_spec.rb} +22 -59
  38. data/spec/savon/soap_fault_spec.rb +94 -0
  39. data/spec/spec_helper.rb +5 -3
  40. data/spec/support/fixture.rb +5 -1
  41. metadata +202 -197
  42. data/lib/savon/config.rb +0 -46
  43. data/lib/savon/error.rb +0 -6
  44. data/lib/savon/hooks/group.rb +0 -68
  45. data/lib/savon/hooks/hook.rb +0 -61
  46. data/lib/savon/http/error.rb +0 -42
  47. data/lib/savon/logger.rb +0 -39
  48. data/lib/savon/null_logger.rb +0 -10
  49. data/lib/savon/soap.rb +0 -21
  50. data/lib/savon/soap/fault.rb +0 -59
  51. data/lib/savon/soap/invalid_response_error.rb +0 -13
  52. data/lib/savon/soap/request.rb +0 -86
  53. data/lib/savon/soap/request_builder.rb +0 -205
  54. data/lib/savon/soap/response.rb +0 -117
  55. data/lib/savon/soap/xml.rb +0 -257
  56. data/spec/savon/config_spec.rb +0 -38
  57. data/spec/savon/hooks/group_spec.rb +0 -71
  58. data/spec/savon/hooks/hook_spec.rb +0 -16
  59. data/spec/savon/http/error_spec.rb +0 -52
  60. data/spec/savon/logger_spec.rb +0 -51
  61. data/spec/savon/savon_spec.rb +0 -33
  62. data/spec/savon/soap/fault_spec.rb +0 -89
  63. data/spec/savon/soap/request_builder_spec.rb +0 -207
  64. data/spec/savon/soap/request_spec.rb +0 -112
  65. data/spec/savon/soap/xml_spec.rb +0 -357
  66. 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