savon 2.2.0 → 2.12.1

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 (60) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/.travis.yml +20 -9
  4. data/CHANGELOG.md +157 -10
  5. data/CONTRIBUTING.md +1 -1
  6. data/Gemfile +10 -2
  7. data/README.md +38 -13
  8. data/donate.png +0 -0
  9. data/lib/savon/builder.rb +81 -15
  10. data/lib/savon/client.rb +6 -2
  11. data/lib/savon/core_ext/string.rb +0 -1
  12. data/lib/savon/header.rb +68 -17
  13. data/lib/savon/log_message.rb +7 -3
  14. data/lib/savon/message.rb +6 -7
  15. data/lib/savon/mock/expectation.rb +12 -2
  16. data/lib/savon/model.rb +4 -0
  17. data/lib/savon/operation.rb +45 -38
  18. data/lib/savon/options.rb +149 -22
  19. data/lib/savon/qualified_message.rb +31 -25
  20. data/lib/savon/request.rb +24 -4
  21. data/lib/savon/request_logger.rb +48 -0
  22. data/lib/savon/response.rb +35 -18
  23. data/lib/savon/soap_fault.rb +11 -11
  24. data/lib/savon/version.rb +1 -3
  25. data/savon.gemspec +12 -11
  26. data/spec/fixtures/response/empty_soap_fault.xml +13 -0
  27. data/spec/fixtures/response/f5.xml +39 -0
  28. data/spec/fixtures/response/no_body.xml +1 -0
  29. data/spec/fixtures/response/soap_fault_funky.xml +8 -0
  30. data/spec/fixtures/wsdl/brand.xml +624 -0
  31. data/spec/fixtures/wsdl/elements_in_types.xml +43 -0
  32. data/spec/fixtures/wsdl/no_message_tag.xml +1267 -0
  33. data/spec/fixtures/wsdl/vies.xml +176 -0
  34. data/spec/integration/centra_spec.rb +67 -0
  35. data/spec/integration/email_example_spec.rb +1 -1
  36. data/spec/integration/random_quote_spec.rb +23 -0
  37. data/spec/integration/stockquote_example_spec.rb +7 -1
  38. data/spec/integration/support/application.rb +1 -1
  39. data/spec/integration/zipcode_example_spec.rb +1 -1
  40. data/spec/savon/builder_spec.rb +50 -0
  41. data/spec/savon/client_spec.rb +78 -0
  42. data/spec/savon/core_ext/string_spec.rb +9 -9
  43. data/spec/savon/features/message_tag_spec.rb +5 -0
  44. data/spec/savon/http_error_spec.rb +2 -2
  45. data/spec/savon/log_message_spec.rb +18 -1
  46. data/spec/savon/message_spec.rb +70 -0
  47. data/spec/savon/mock_spec.rb +31 -0
  48. data/spec/savon/model_spec.rb +28 -0
  49. data/spec/savon/operation_spec.rb +69 -3
  50. data/spec/savon/options_spec.rb +515 -87
  51. data/spec/savon/qualified_message_spec.rb +101 -0
  52. data/spec/savon/request_logger_spec.rb +37 -0
  53. data/spec/savon/request_spec.rb +85 -10
  54. data/spec/savon/response_spec.rb +118 -27
  55. data/spec/savon/soap_fault_spec.rb +25 -5
  56. data/spec/savon/softlayer_spec.rb +27 -0
  57. data/spec/spec_helper.rb +5 -2
  58. data/spec/support/adapters.rb +48 -0
  59. data/spec/support/integration.rb +1 -1
  60. metadata +76 -93
@@ -2,44 +2,50 @@ require "gyoku"
2
2
 
3
3
  module Savon
4
4
  class QualifiedMessage
5
-
6
5
  def initialize(types, used_namespaces, key_converter)
7
- @types = types
6
+ @types = types
8
7
  @used_namespaces = used_namespaces
9
- @key_converter = key_converter
8
+ @key_converter = key_converter
10
9
  end
11
10
 
12
11
  def to_hash(hash, path)
13
- return unless hash
14
- return hash.map { |value| to_hash(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
- translated_key = Gyoku.xml_tag(key, :key_converter => @key_converter).to_s
19
- newpath = path + [translated_key]
20
-
21
- if @used_namespaces[newpath]
22
- newhash.merge(
23
- "#{@used_namespaces[newpath]}:#{translated_key}" =>
24
- to_hash(value, @types[newpath] ? [@types[newpath]] : newpath)
25
- )
12
+ return hash unless hash
13
+ return hash.map { |value| to_hash(value, path) } if hash.is_a?(Array)
14
+ return hash.to_s unless hash.is_a?(Hash)
15
+
16
+ hash.each_with_object({}) do |(key, value), newhash|
17
+ case key
18
+ when :order!
19
+ newhash[key] = add_namespaces_to_values(value, path)
20
+ when :attributes!, :content!
21
+ newhash[key] = to_hash(value, path)
26
22
  else
27
- add_namespaces_to_values(value, path) if key == :order!
28
- newhash.merge(key => value)
23
+ if key.to_s =~ /!$/
24
+ newhash[key] = value
25
+ else
26
+ translated_key = translate_tag(key)
27
+ newkey = add_namespaces_to_values(key, path).first
28
+ newpath = path + [translated_key]
29
+ newhash[newkey] = to_hash(value, @types[newpath] ? [@types[newpath]] : newpath)
30
+ end
29
31
  end
32
+ newhash
30
33
  end
31
34
  end
32
35
 
33
36
  private
34
37
 
35
- def add_namespaces_to_values(values, path)
36
- values.collect! { |value|
37
- camelcased_value = Gyoku.xml_tag(value, :key_converter => @key_converter)
38
- namespace_path = path + [camelcased_value.to_s]
39
- namespace = @used_namespaces[namespace_path]
40
- "#{namespace.blank? ? '' : namespace + ":"}#{camelcased_value}"
41
- }
38
+ def translate_tag(key)
39
+ Gyoku.xml_tag(key, :key_converter => @key_converter).to_s
42
40
  end
43
41
 
42
+ def add_namespaces_to_values(values, path)
43
+ Array(values).collect do |value|
44
+ translated_value = translate_tag(value)
45
+ namespace_path = path + [translated_value]
46
+ namespace = @used_namespaces[namespace_path]
47
+ namespace.blank? ? value : "#{namespace}:#{translated_value}"
48
+ end
49
+ end
44
50
  end
45
51
  end
@@ -26,10 +26,16 @@ module Savon
26
26
  def configure_ssl
27
27
  @http_request.auth.ssl.ssl_version = @globals[:ssl_version] if @globals.include? :ssl_version
28
28
  @http_request.auth.ssl.verify_mode = @globals[:ssl_verify_mode] if @globals.include? :ssl_verify_mode
29
+ @http_request.auth.ssl.ciphers = @globals[:ssl_ciphers] if @globals.include? :ssl_ciphers
29
30
 
30
31
  @http_request.auth.ssl.cert_key_file = @globals[:ssl_cert_key_file] if @globals.include? :ssl_cert_key_file
32
+ @http_request.auth.ssl.cert_key = @globals[:ssl_cert_key] if @globals.include? :ssl_cert_key
31
33
  @http_request.auth.ssl.cert_file = @globals[:ssl_cert_file] if @globals.include? :ssl_cert_file
34
+ @http_request.auth.ssl.cert = @globals[:ssl_cert] if @globals.include? :ssl_cert
32
35
  @http_request.auth.ssl.ca_cert_file = @globals[:ssl_ca_cert_file] if @globals.include? :ssl_ca_cert_file
36
+ @http_request.auth.ssl.ca_cert_path = @globals[:ssl_ca_cert_path] if @globals.include? :ssl_ca_cert_path
37
+ @http_request.auth.ssl.ca_cert = @globals[:ssl_ca_cert] if @globals.include? :ssl_ca_cert
38
+ @http_request.auth.ssl.cert_store = @globals[:ssl_cert_store] if @globals.include? :ssl_cert_store
33
39
 
34
40
  @http_request.auth.ssl.cert_key_password = @globals[:ssl_cert_key_password] if @globals.include? :ssl_cert_key_password
35
41
  end
@@ -37,8 +43,14 @@ module Savon
37
43
  def configure_auth
38
44
  @http_request.auth.basic(*@globals[:basic_auth]) if @globals.include? :basic_auth
39
45
  @http_request.auth.digest(*@globals[:digest_auth]) if @globals.include? :digest_auth
46
+ @http_request.auth.ntlm(*@globals[:ntlm]) if @globals.include? :ntlm
40
47
  end
41
48
 
49
+ def configure_redirect_handling
50
+ if @globals.include? :follow_redirects
51
+ @http_request.follow_redirect = @globals[:follow_redirects]
52
+ end
53
+ end
42
54
  end
43
55
 
44
56
  class WSDLRequest < HTTPRequest
@@ -46,12 +58,19 @@ module Savon
46
58
  def build
47
59
  configure_proxy
48
60
  configure_timeouts
61
+ configure_headers
49
62
  configure_ssl
50
63
  configure_auth
64
+ configure_redirect_handling
51
65
 
52
66
  @http_request
53
67
  end
54
68
 
69
+ private
70
+
71
+ def configure_headers
72
+ @http_request.headers = @globals[:headers] if @globals.include? :headers
73
+ end
55
74
  end
56
75
 
57
76
  class SOAPRequest < HTTPRequest
@@ -63,11 +82,12 @@ module Savon
63
82
 
64
83
  def build(options = {})
65
84
  configure_proxy
66
- configure_cookies options[:cookies]
67
85
  configure_timeouts
68
- configure_headers options[:soap_action]
86
+ configure_headers options[:soap_action], options[:headers]
87
+ configure_cookies options[:cookies]
69
88
  configure_ssl
70
89
  configure_auth
90
+ configure_redirect_handling
71
91
 
72
92
  @http_request
73
93
  end
@@ -78,11 +98,11 @@ module Savon
78
98
  @http_request.set_cookies(cookies) if cookies
79
99
  end
80
100
 
81
- def configure_headers(soap_action)
101
+ def configure_headers(soap_action, headers)
82
102
  @http_request.headers = @globals[:headers] if @globals.include? :headers
103
+ @http_request.headers.merge!(headers) if headers
83
104
  @http_request.headers["SOAPAction"] ||= %{"#{soap_action}"} if soap_action
84
105
  @http_request.headers["Content-Type"] ||= CONTENT_TYPE[@globals[:soap_version]] % @globals[:encoding]
85
106
  end
86
-
87
107
  end
88
108
  end
@@ -0,0 +1,48 @@
1
+ require "savon/log_message"
2
+
3
+ module Savon
4
+ class RequestLogger
5
+
6
+ def initialize(globals)
7
+ @globals = globals
8
+ end
9
+
10
+ def log(request, &http_request)
11
+ log_request(request) if log?
12
+ response = http_request.call
13
+ log_response(response) if log?
14
+
15
+ response
16
+ end
17
+
18
+ def logger
19
+ @globals[:logger]
20
+ end
21
+
22
+ def log?
23
+ @globals[:log]
24
+ end
25
+
26
+ private
27
+
28
+ def log_request(request)
29
+ logger.info { "SOAP request: #{request.url}" }
30
+ logger.info { headers_to_log(request.headers) }
31
+ logger.debug { body_to_log(request.body) }
32
+ end
33
+
34
+ def log_response(response)
35
+ logger.info { "SOAP response (status #{response.code})" }
36
+ logger.debug { body_to_log(response.body) }
37
+ end
38
+
39
+ def headers_to_log(headers)
40
+ headers.map { |key, value| "#{key}: #{value}" }.join(", ")
41
+ end
42
+
43
+ def body_to_log(body)
44
+ LogMessage.new(body, @globals[:filters], @globals[:pretty_print_xml]).to_s
45
+ end
46
+
47
+ end
48
+ end
@@ -10,18 +10,19 @@ module Savon
10
10
  @globals = globals
11
11
  @locals = locals
12
12
 
13
+ build_soap_and_http_errors!
13
14
  raise_soap_and_http_errors! if @globals[:raise_errors]
14
15
  end
15
16
 
16
- attr_reader :http, :globals, :locals
17
+ attr_reader :http, :globals, :locals, :soap_fault, :http_error
17
18
 
18
19
  def success?
19
20
  !soap_fault? && !http_error?
20
21
  end
21
- alias successful? success?
22
+ alias_method :successful?, :success?
22
23
 
23
24
  def soap_fault?
24
- SOAPFault.present? @http
25
+ SOAPFault.present?(@http, xml)
25
26
  end
26
27
 
27
28
  def http_error?
@@ -29,15 +30,14 @@ module Savon
29
30
  end
30
31
 
31
32
  def header
32
- raise_invalid_response_error! unless hash.key? :envelope
33
- hash[:envelope][:header]
33
+ find('Header')
34
34
  end
35
35
 
36
36
  def body
37
- raise_invalid_response_error! unless hash.key? :envelope
38
- hash[:envelope][:body]
37
+ find('Body')
39
38
  end
40
- alias to_hash body
39
+
40
+ alias_method :to_hash, :body
41
41
 
42
42
  def to_array(*path)
43
43
  result = path.inject body do |memo, key|
@@ -49,30 +49,45 @@ module Savon
49
49
  end
50
50
 
51
51
  def hash
52
- @hash ||= nori.parse(to_xml)
52
+ @hash ||= nori.parse(xml)
53
53
  end
54
54
 
55
- def to_xml
55
+ def xml
56
56
  @http.body
57
57
  end
58
58
 
59
+ alias_method :to_xml, :xml
60
+ alias_method :to_s, :xml
61
+
59
62
  def doc
60
- @doc ||= Nokogiri.XML(to_xml)
63
+ @doc ||= Nokogiri.XML(xml)
61
64
  end
62
65
 
63
66
  def xpath(path, namespaces = nil)
64
67
  doc.xpath(path, namespaces || xml_namespaces)
65
68
  end
66
69
 
70
+ def find(*path)
71
+ envelope = nori.find(hash, 'Envelope')
72
+ raise_invalid_response_error! unless envelope.is_a?(Hash)
73
+
74
+ nori.find(envelope, *path)
75
+ end
76
+
67
77
  private
68
78
 
79
+ def build_soap_and_http_errors!
80
+ @soap_fault = SOAPFault.new(@http, nori, xml) if soap_fault?
81
+ @http_error = HTTPError.new(@http) if http_error?
82
+ end
83
+
69
84
  def raise_soap_and_http_errors!
70
- raise SOAPFault.new(@http, nori) if soap_fault?
71
- raise HTTPError.new(@http) if http_error?
85
+ raise soap_fault if soap_fault?
86
+ raise http_error if http_error?
72
87
  end
73
88
 
74
89
  def raise_invalid_response_error!
75
- raise InvalidResponseError, "Unable to parse response body:\n" + to_xml.inspect
90
+ raise InvalidResponseError, "Unable to parse response body:\n" + xml.inspect
76
91
  end
77
92
 
78
93
  def xml_namespaces
@@ -83,10 +98,12 @@ module Savon
83
98
  return @nori if @nori
84
99
 
85
100
  nori_options = {
86
- :strip_namespaces => @globals[:strip_namespaces],
87
- :convert_tags_to => @globals[:convert_response_tags_to],
88
- :advanced_typecasting => @locals[:advanced_typecasting],
89
- :parser => @locals[:response_parser]
101
+ :delete_namespace_attributes => @globals[:delete_namespace_attributes],
102
+ :strip_namespaces => @globals[:strip_namespaces],
103
+ :convert_tags_to => @globals[:convert_response_tags_to],
104
+ :convert_attributes_to => @globals[:convert_attributes_to],
105
+ :advanced_typecasting => @locals[:advanced_typecasting],
106
+ :parser => @locals[:response_parser]
90
107
  }
91
108
 
92
109
  non_nil_nori_options = nori_options.reject { |_, value| value.nil? }
@@ -1,31 +1,31 @@
1
- require "savon"
2
-
3
1
  module Savon
4
2
  class SOAPFault < Error
5
3
 
6
- def self.present?(http)
7
- fault_node = http.body.include?("Fault>")
8
- soap1_fault = http.body.include?("faultcode>") && http.body.include?("faultstring>")
9
- soap2_fault = http.body.include?("Code>") && http.body.include?("Reason>")
4
+ def self.present?(http, xml = nil)
5
+ xml ||= http.body
6
+ fault_node = xml.include?("Fault>")
7
+ soap1_fault = xml.match(/faultcode\/?\>/) && xml.match(/faultstring\/?\>/)
8
+ soap2_fault = xml.include?("Code>") && xml.include?("Reason>")
10
9
 
11
10
  fault_node && (soap1_fault || soap2_fault)
12
11
  end
13
12
 
14
- def initialize(http, nori)
13
+ def initialize(http, nori, xml = nil)
14
+ @xml = xml
15
15
  @http = http
16
16
  @nori = nori
17
17
  end
18
18
 
19
- attr_reader :http, :nori
19
+ attr_reader :http, :nori, :xml
20
20
 
21
21
  def to_s
22
- fault = nori.find(to_hash, 'Fault')
22
+ fault = nori.find(to_hash, 'Fault') || nori.find(to_hash, 'ServiceFault')
23
23
  message_by_version(fault)
24
24
  end
25
25
 
26
26
  def to_hash
27
- parsed = nori.parse(@http.body)
28
- nori.find(parsed, 'Envelope', 'Body')
27
+ parsed = nori.parse(xml || http.body)
28
+ nori.find(parsed, 'Envelope', 'Body') || {}
29
29
  end
30
30
 
31
31
  private
@@ -1,5 +1,3 @@
1
1
  module Savon
2
-
3
- VERSION = "2.2.0"
4
-
2
+ VERSION = '2.12.1'
5
3
  end
@@ -12,24 +12,25 @@ Gem::Specification.new do |s|
12
12
  s.homepage = "http://savonrb.com"
13
13
  s.summary = "Heavy metal SOAP client"
14
14
  s.description = s.summary
15
+ s.required_ruby_version = '>= 1.9.2'
15
16
 
16
17
  s.rubyforge_project = s.name
18
+ s.license = 'MIT'
17
19
 
18
- s.add_dependency "nori", "~> 2.1.0"
19
- s.add_dependency "httpi", "~> 2.0.2"
20
- s.add_dependency "wasabi", "~> 3.1.0"
21
- s.add_dependency "akami", "~> 1.2.0"
22
- s.add_dependency "gyoku", "~> 1.0.0"
23
-
20
+ s.add_dependency "nori", "~> 2.4"
21
+ s.add_dependency "httpi", "~> 2.3"
22
+ s.add_dependency "wasabi", "~> 3.4"
23
+ s.add_dependency "akami", "~> 1.2"
24
+ s.add_dependency "gyoku", "~> 1.2"
24
25
  s.add_dependency "builder", ">= 2.1.2"
25
- s.add_dependency "nokogiri", ">= 1.4.0"
26
+ s.add_dependency "nokogiri", ">= 1.8.1"
26
27
 
27
28
  s.add_development_dependency "rack"
28
- s.add_development_dependency "puma", "2.0.0.b4"
29
+ s.add_development_dependency "puma", "~> 3.0"
29
30
 
30
- s.add_development_dependency "rake", "~> 0.9"
31
- s.add_development_dependency "rspec", "~> 2.10"
32
- s.add_development_dependency "mocha", "~> 0.11"
31
+ s.add_development_dependency "rake", "~> 10.1"
32
+ s.add_development_dependency "rspec", "~> 2.14"
33
+ s.add_development_dependency "mocha", "~> 0.14"
33
34
  s.add_development_dependency "json", "~> 1.7"
34
35
 
35
36
  ignores = File.readlines(".gitignore").grep(/\S+/).map(&:chomp)
@@ -0,0 +1,13 @@
1
+ <SOAP-ENV:Envelope SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
2
+ xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
3
+ xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
4
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema">
6
+ <SOAP-ENV:Body>
7
+ <SOAP-ENV:Fault>
8
+ <faultcode/>
9
+ <faultstring/>
10
+ <detail><soapVal><ERRNO xsi:type="xsd:string">80:1289245853:55</ERRNO></soapVal></detail>
11
+ </SOAP-ENV:Fault>
12
+ </SOAP-ENV:Body>
13
+ </SOAP-ENV:Envelope>
@@ -0,0 +1,39 @@
1
+ <E:Envelope
2
+ xmlns:E="http://schemas.xmlsoap.org/soap/envelope/"
3
+ xmlns:A="http://schemas.xmlsoap.org/soap/encoding/"
4
+ xmlns:s="http://www.w3.org/2001/XMLSchema-instance"
5
+ xmlns:y="http://www.w3.org/2001/XMLSchema"
6
+ xmlns:iControl="urn:iControl"
7
+ E:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
8
+ <E:Body>
9
+ <m:get_agent_listen_addressResponse
10
+ xmlns:m="urn:iControl:Management/SNMPConfiguration">
11
+ <return
12
+ s:type="A:Array"
13
+ A:arrayType="iControl:Management.SNMPConfiguration.AgentListenAddressPort[2]">
14
+ <item>
15
+ <transport
16
+ s:type="iControl:Management.SNMPConfiguration.TransportType">TRANSPORT_TCP6</transport>
17
+ <ipport
18
+ s:type="iControl:Common.IPPortDefinition">
19
+ <address
20
+ s:type="y:string"></address>
21
+ <port
22
+ s:type="y:long">161</port>
23
+ </ipport>
24
+ </item>
25
+ <item>
26
+ <transport
27
+ s:type="iControl:Management.SNMPConfiguration.TransportType">TRANSPORT_UDP6</transport>
28
+ <ipport
29
+ s:type="iControl:Common.IPPortDefinition">
30
+ <address
31
+ s:type="y:string"></address>
32
+ <port
33
+ s:type="y:long">161</port>
34
+ </ipport>
35
+ </item>
36
+ </return>
37
+ </m:get_agent_listen_addressResponse>
38
+ </E:Body>
39
+ </E:Envelope>