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.
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