savon 0.6.8 → 0.7.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 CHANGED
@@ -1,3 +1,33 @@
1
+ == 0.7.0 (2010-01-09)
2
+ This version comes with several changes to the public API!
3
+ Pay attention to the following list and read the updated Wiki: http://wiki.github.com/rubiii/savon
4
+
5
+ * Changed how Savon::WSDL can be disabled. Instead of disabling the WSDL globally/per request via two
6
+ different methods, you now simply append an exclamation mark (!) to your SOAP call: client.get_all_users!
7
+ Make sure you know what you're doing because when the WSDL is disabled, Savon does not know about which
8
+ SOAP actions are valid and just dispatches everything.
9
+ * The Net::HTTP object used by Savon::Request to retrieve WSDL documents and execute SOAP calls is now public.
10
+ While this makes the library even more flexible, it also comes with two major changes:
11
+ * SSL client authentication needs to be defined directly on the Net::HTTP object:
12
+ client.request.http.client_cert = ...
13
+ I added a shortcut method for setting all options through a Hash similar to the previous implementation:
14
+ client.request.http.ssl_client_auth :client_cert => ...
15
+ * Open and read timeouts also need to be set on the Net::HTTP object:
16
+ client.request.http.open_timeout = 30
17
+ client.request.http.read_timeout = 30
18
+ * Please refer to the Net::HTTP documentation for more details:
19
+ http://www.ruby-doc.org/stdlib/libdoc/net/http/rdoc/index.html
20
+ * Thanks to JulianMorrison, Savon now supports HTTP basic authentication:
21
+ client.request.http.basic_auth "username", "password"
22
+ * Julian also added a way to explicitly specify the order of Hash keys and values, so you should now be able
23
+ to work with services requiring a specific order of input parameters while still using Hash input.
24
+ For example: client.find_user { |soap| soap.body = { :name => "Lucy", :id => 666, :@inorder => [:id, :name] } }
25
+ * Savon::Response#to_hash now returns the content inside of "soap:Body" instead of trying to go one level
26
+ deeper and return it's content. The previous implementation only worked when the "soap:Body" element
27
+ contained a single child. See: http://github.com/rubiii/savon/issues#issue/17
28
+ * Added Savon::SOAP#namespace as a shortcut for setting the "xmlns:wsdl" namespace.
29
+ Usage example: soap.namespace = "http://example.com"
30
+
1
31
  == 0.6.8 (2010-01-01)
2
32
  * Improved specifications for various kinds of WSDL documents.
3
33
  * Added support for SOAP endpoints which are different than the WSDL endpoint of a service.
@@ -4,6 +4,10 @@ h4. Heavy metal Ruby SOAP client library
4
4
 
5
5
  p. "RDoc":http://rdoc.info/projects/rubiii/savon | "Wiki":http://wiki.github.com/rubiii/savon | "ToDo":http://rubiii.tadalist.com/lists/1459816/public | "Metrics":http://getcaliper.com/caliper/project?repo=git://github.com/rubiii/savon.git
6
6
 
7
+ h2. Warning
8
+
9
+ p. Savon 0.7.0 comes with several changes to the public API. Pay attention to the CHANGELOG and the updated Wiki.
10
+
7
11
  h2. Installation
8
12
 
9
13
  bc. $ gem install savon
@@ -20,7 +24,7 @@ p. More information: "Client":http://wiki.github.com/rubiii/savon/client
20
24
 
21
25
  h2. Calling a SOAP action
22
26
 
23
- p. Assuming your service applies to the "Defaults":http://wiki.github.com/rubiii/savon/defaults, you can now call any available SOAP action.
27
+ p. Assuming your service applies to the defaults, you can now call any available SOAP action.
24
28
 
25
29
  bc. response = client.get_all_users
26
30
 
@@ -67,5 +71,5 @@ More information: "Errors":http://wiki.github.com/rubiii/savon/errors
67
71
 
68
72
  h2. Logging
69
73
 
70
- p. Savon logs each request and response to STDOUT. But there are a couple of options to change the default behaviour.
74
+ p. Savon logs each request and response to STDOUT. But there are a couple of options to change the default behavior.
71
75
  More information: "Logging":http://wiki.github.com/rubiii/savon/logging
data/Rakefile CHANGED
@@ -1,24 +1,42 @@
1
- require "rubygems"
2
1
  require "rake"
3
2
  require "spec/rake/spectask"
4
3
  require "spec/rake/verify_rcov"
5
4
  require "rake/rdoctask"
6
5
 
7
- task :default => :spec_verify_rcov
6
+ task :default => :spec_verify
8
7
 
9
8
  Spec::Rake::SpecTask.new do |spec|
10
- spec.spec_files = FileList["spec/**/*_spec.rb"]
9
+ spec.spec_files = FileList["spec/{savon}/**/*_spec.rb"]
11
10
  spec.spec_opts << "--color"
12
11
  spec.libs += ["lib", "spec"]
13
12
  spec.rcov = true
14
13
  spec.rcov_dir = "rcov"
15
14
  end
16
15
 
17
- RCov::VerifyTask.new(:spec_verify_rcov => :spec) do |verify|
16
+ RCov::VerifyTask.new(:spec_verify => :spec) do |verify|
18
17
  verify.threshold = 100.0
19
18
  verify.index_html = "rcov/index.html"
20
19
  end
21
20
 
21
+ desc "Run integration specs using WEBrick"
22
+ task :spec_integration do
23
+ pid = fork { exec "ruby spec/integration/server.rb" }
24
+ sleep 10 # wait until the server is actually ready
25
+ begin
26
+ task(:run_integration_spec).invoke
27
+ ensure
28
+ Process.kill "TERM", pid
29
+ Process.wait pid
30
+ end
31
+ end
32
+
33
+ desc "" # make this task invisible for "rake -T"
34
+ Spec::Rake::SpecTask.new(:run_integration_spec) do |spec|
35
+ spec.spec_files = FileList["spec/{integration}/**/*_spec.rb"]
36
+ spec.spec_opts << "--color"
37
+ spec.libs += ["lib", "spec"]
38
+ end
39
+
22
40
  Rake::RDocTask.new do |rdoc|
23
41
  rdoc.title = "Savon"
24
42
  rdoc.rdoc_dir = "rdoc"
@@ -18,7 +18,7 @@ module Savon
18
18
  end
19
19
 
20
20
  # standard libs
21
- %w(logger net/http net/https openssl uri base64 digest/sha1 rexml/document).each do |lib|
21
+ %w(logger net/https openssl base64 digest/sha1 rexml/document).each do |lib|
22
22
  require lib
23
23
  end
24
24
 
@@ -6,19 +6,6 @@ module Savon
6
6
  # with SOAP services and XML.
7
7
  class Client
8
8
 
9
- # Global setting of whether to use Savon::WSDL.
10
- @@wsdl = true
11
-
12
- # Sets the global setting of whether to use Savon::WSDL.
13
- def self.wsdl=(wsdl)
14
- @@wsdl = wsdl
15
- end
16
-
17
- # Returns the global setting of whether to use Savon::WSDL.
18
- def self.wsdl?
19
- @@wsdl
20
- end
21
-
22
9
  # Expects a SOAP +endpoint+ String. Also accepts an optional Hash of
23
10
  # +options+ for specifying a proxy server and SSL client authentication.
24
11
  def initialize(endpoint, options = {})
@@ -26,17 +13,12 @@ module Savon
26
13
  @wsdl = WSDL.new @request
27
14
  end
28
15
 
29
- # Accessor for Savon::WSDL.
30
- attr_accessor :wsdl
16
+ # Returns the Savon::WSDL.
17
+ attr_reader :wsdl
31
18
 
32
19
  # Returns the Savon::Request.
33
20
  attr_reader :request
34
21
 
35
- # Returns whether to use Savon::WSDL.
36
- def wsdl?
37
- self.class.wsdl? && @wsdl
38
- end
39
-
40
22
  # Returns +true+ for available methods and SOAP actions.
41
23
  def respond_to?(method)
42
24
  return true if @wsdl.respond_to? method
@@ -47,22 +29,35 @@ module Savon
47
29
 
48
30
  # Dispatches requests to SOAP actions matching a given +method+ name.
49
31
  def method_missing(method, *args, &block) #:doc:
50
- super if wsdl? && !@wsdl.respond_to?(method)
32
+ soap_call = soap_call_from method.to_s
33
+ super if @wsdl.enabled? && !@wsdl.respond_to?(soap_call)
51
34
 
52
- setup_objects operation_from(method), &block
35
+ setup_objects operation_from(soap_call), &block
53
36
  Response.new @request.soap(@soap)
54
37
  end
55
38
 
39
+ # Sets whether to use Savon::WSDL by a given +method+ name and
40
+ # removes exclamation marks from the given +method+ name.
41
+ def soap_call_from(method)
42
+ if method[-1, 1] == "!"
43
+ @wsdl.enabled = false
44
+ method[0, method.length-1].to_sym
45
+ else
46
+ @wsdl.enabled = true
47
+ method.to_sym
48
+ end
49
+ end
50
+
56
51
  # Returns a SOAP operation Hash containing the SOAP action and input
57
- # for a given +method+.
58
- def operation_from(method)
59
- return @wsdl.operations[method] if wsdl?
60
- { :action => method.to_soap_key, :input => method.to_soap_key }
52
+ # for a given +soap_call+.
53
+ def operation_from(soap_call)
54
+ return @wsdl.operations[soap_call] if @wsdl.enabled?
55
+ { :action => soap_call.to_soap_key, :input => soap_call.to_soap_key }
61
56
  end
62
57
 
63
58
  # Returns the SOAP endpoint.
64
59
  def soap_endpoint
65
- wsdl? ? @wsdl.soap_endpoint : @request.endpoint
60
+ @wsdl.enabled? ? @wsdl.soap_endpoint : @request.endpoint
66
61
  end
67
62
 
68
63
  # Expects a SOAP operation Hash and sets up Savon::SOAP and Savon::WSSE.
@@ -73,7 +68,7 @@ module Savon
73
68
 
74
69
  yield_objects &block if block
75
70
 
76
- @soap.namespaces["xmlns:wsdl"] ||= @wsdl.namespace_uri if wsdl?
71
+ @soap.namespaces["xmlns:wsdl"] ||= @wsdl.namespace_uri if @wsdl.enabled?
77
72
  @soap.wsse = @wsse
78
73
  end
79
74
 
@@ -1,3 +1,3 @@
1
- %w(object string symbol datetime hash uri).each do |file|
1
+ %w(object string symbol datetime hash uri net_http).each do |file|
2
2
  require File.dirname(__FILE__) + "/core_ext/#{file}"
3
3
  end
@@ -1,29 +1,34 @@
1
1
  class Hash
2
2
 
3
- # Expects an Array of Regexp Objects of which every Regexp recursively
4
- # matches a key to be accessed. Returns the value of the last Regexp filter
5
- # found in the Hash or an empty Hash in case the path of Regexp filters
6
- # did not match the Hash structure.
7
- def find_regexp(regexp)
8
- regexp = [regexp] unless regexp.kind_of? Array
9
- result = dup
10
-
11
- regexp.each do |pattern|
12
- result_key = result.keys.find { |key| key.to_s.match pattern }
13
- result = result[result_key] ? result[result_key] : {}
14
- end
15
- result
3
+ # Error message for missing :@inorder elements.
4
+ InOrderMissing = "Missing elements in :@inorder %s"
5
+
6
+ # Error message for spurious :@inorder elements.
7
+ InOrderSpurious = "Spurious elements in :@inorder %s"
8
+
9
+ # Returns the values from the 'soap:Body' element or an empty Hash
10
+ # in case the node could not be found.
11
+ def find_soap_body
12
+ envelope = self[self.keys.first] || {}
13
+ body_key = envelope.keys.find { |key| /.+:Body/ =~ key } rescue nil
14
+ body_key ? envelope[body_key].map_soap_response : {}
16
15
  end
17
16
 
18
17
  # Returns the Hash translated into SOAP request compatible XML.
19
18
  #
20
- # === Example
19
+ # To control the order of output, add a key of :@inorder with
20
+ # the value being an Array listing keys in order.
21
+ #
22
+ # === Examples
21
23
  #
22
24
  # { :find_user => { :id => 666 } }.to_soap_xml
23
25
  # => "<findUser><id>666</id></findUser>"
26
+ #
27
+ # { :find_user => { :name => "Lucy", :id => 666, :@inorder => [:id, :name] } }.to_soap_xml
28
+ # => "<findUser><id>666</id><name>Lucy</name></findUser>"
24
29
  def to_soap_xml
25
30
  @soap_xml = Builder::XmlMarkup.new
26
- each { |key, value| nested_data_to_soap_xml key, value }
31
+ inorder(self).each { |key| nested_data_to_soap_xml key, self[key] }
27
32
  @soap_xml.target!
28
33
  end
29
34
 
@@ -49,14 +54,25 @@ private
49
54
  def nested_data_to_soap_xml(key, value)
50
55
  case value
51
56
  when Array
52
- value.map { |sitem| nested_data_to_soap_xml key, sitem }
57
+ value.map { |subitem| nested_data_to_soap_xml key, subitem }
53
58
  when Hash
54
59
  @soap_xml.tag!(key.to_soap_key) do
55
- value.each { |subkey, subvalue| nested_data_to_soap_xml subkey, subvalue }
60
+ inorder(value).each { |subkey| nested_data_to_soap_xml subkey, value[subkey] }
56
61
  end
57
62
  else
58
63
  @soap_xml.tag!(key.to_soap_key) { @soap_xml << value.to_soap_value }
59
64
  end
60
65
  end
61
66
 
67
+ # Takes a +hash+, removes the :@inorder marker and returns its keys.
68
+ # Raises an error in case an :@inorder Array does not match the Hash keys.
69
+ def inorder(hash)
70
+ inorder = hash.delete :@inorder
71
+ hash_keys = hash.keys
72
+ inorder = hash_keys unless inorder.kind_of? Array
73
+ raise InOrderMissing % (hash_keys - inorder).inspect unless (hash_keys - inorder).empty?
74
+ raise InOrderSpurious % (inorder - hash_keys).inspect unless (inorder - hash_keys).empty?
75
+ inorder
76
+ end
77
+
62
78
  end
@@ -0,0 +1,20 @@
1
+ module Net
2
+ class HTTP
3
+
4
+ # Sets the endpoint +address+ and +port+.
5
+ def endpoint(address, port)
6
+ @address, @port = address, port
7
+ end
8
+
9
+ # Convenience method for setting SSL client authentication
10
+ # through a Hash of +options+.
11
+ def ssl_client_auth(options)
12
+ self.use_ssl = true
13
+ self.cert = options[:cert] if options[:cert]
14
+ self.key = options[:key] if options[:key]
15
+ self.ca_file = options[:ca_file] if options[:ca_file]
16
+ self.verify_mode = options[:verify_mode] if options[:verify_mode]
17
+ end
18
+
19
+ end
20
+ end
@@ -47,12 +47,11 @@ module Savon
47
47
  @@log_level
48
48
  end
49
49
 
50
- # Expects a SOAP +endpoint+ String. Also accepts an optional Hash of
51
- # +options+ for specifying a proxy server and SSL client authentication.
50
+ # Expects a SOAP +endpoint+ String. Also accepts an optional Hash
51
+ # of +options+ for specifying a proxy server.
52
52
  def initialize(endpoint, options = {})
53
53
  @endpoint = URI endpoint
54
- @proxy = options[:proxy] ? URI(options[:proxy]) : URI("")
55
- @ssl = options[:ssl] if options[:ssl]
54
+ @proxy = options[:proxy] ? URI(options[:proxy]) : URI("")
56
55
  end
57
56
 
58
57
  # Returns the endpoint URI.
@@ -61,35 +60,45 @@ module Savon
61
60
  # Returns the proxy URI.
62
61
  attr_reader :proxy
63
62
 
64
- # Accessor for HTTP open timeout.
65
- attr_accessor :open_timeout
66
-
67
- # Accessor for HTTP read timeout.
68
- attr_accessor :read_timeout
63
+ # Sets the +username+ and +password+ for HTTP basic authentication.
64
+ def basic_auth(username, password)
65
+ @basic_auth = [username, password]
66
+ end
69
67
 
70
- # Retrieves WSDL document and returns the Net::HTTPResponse.
68
+ # Retrieves WSDL document and returns the Net::HTTP response.
71
69
  def wsdl
72
70
  log "Retrieving WSDL from: #{@endpoint}"
73
- http.get @endpoint.to_s
71
+ http.endpoint @endpoint.host, @endpoint.port
72
+ http.use_ssl = @endpoint.ssl?
73
+ http.start { |h| h.request request(:wsdl) }
74
74
  end
75
75
 
76
76
  # Executes a SOAP request using a given Savon::SOAP instance and
77
- # returns the Net::HTTPResponse.
77
+ # returns the Net::HTTP response.
78
78
  def soap(soap)
79
79
  @soap = soap
80
+ http.endpoint @soap.endpoint.host, @soap.endpoint.port
81
+ http.use_ssl = @soap.endpoint.ssl?
80
82
 
81
83
  log_request
82
- @response = http(@soap.endpoint).request_post @soap.endpoint.path, @soap.to_xml, http_header
84
+ @response = http.start do |h|
85
+ h.request request(:soap) { |request| request.body = @soap.to_xml }
86
+ end
83
87
  log_response
84
88
  @response
85
89
  end
86
90
 
91
+ # Returns the Net::HTTP object.
92
+ def http
93
+ @http ||= Net::HTTP::Proxy(@proxy.host, @proxy.port).new @endpoint.host, @endpoint.port
94
+ end
95
+
87
96
  private
88
97
 
89
98
  # Logs the SOAP request.
90
99
  def log_request
91
100
  log "SOAP request: #{@soap.endpoint}"
92
- log http_header.map { |key, value| "#{key}: #{value}" }.join( ", " )
101
+ log http_header.map { |key, value| "#{key}: #{value}" }.join(", ")
93
102
  log @soap.to_xml
94
103
  end
95
104
 
@@ -99,33 +108,22 @@ module Savon
99
108
  log @response.body
100
109
  end
101
110
 
102
- # Returns a Net::HTTP instance for a given +endpoint+.
103
- def http(endpoint = @endpoint)
104
- @http = Net::HTTP::Proxy(@proxy.host, @proxy.port).new endpoint.host, endpoint.port
105
- set_http_timeout
106
- set_ssl_options endpoint.ssl?
107
- set_ssl_authentication if @ssl
108
- @http
109
- end
110
-
111
- # Sets HTTP open and read timeout.
112
- def set_http_timeout
113
- @http.open_timeout = @open_timeout if @open_timeout
114
- @http.read_timeout = @read_timeout if @read_timeout
115
- end
116
-
117
- # Sets basic SSL options to the +@http+ instance.
118
- def set_ssl_options(use_ssl)
119
- @http.use_ssl = use_ssl
120
- @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
111
+ # Returns a Net::HTTP request for a given +type+. Yields the request
112
+ # to an optional block.
113
+ def request(type)
114
+ request = case type
115
+ when :wsdl then Net::HTTP::Get.new wsdl_endpoint
116
+ when :soap then Net::HTTP::Post.new @soap.endpoint.path, http_header
117
+ end
118
+ request.basic_auth *@basic_auth if @basic_auth
119
+ yield request if block_given?
120
+ request
121
121
  end
122
122
 
123
- # Sets SSL client authentication to the +@http+ instance.
124
- def set_ssl_authentication
125
- @http.verify_mode = @ssl[:verify] if @ssl[:verify].kind_of? Integer
126
- @http.cert = @ssl[:client_cert] if @ssl[:client_cert]
127
- @http.key = @ssl[:client_key] if @ssl[:client_key]
128
- @http.ca_file = @ssl[:ca_file] if @ssl[:ca_file]
123
+ # Returns the WSDL endpoint.
124
+ def wsdl_endpoint
125
+ return @endpoint.path unless @endpoint.query
126
+ "#{@endpoint.path}?#{@endpoint.query}"
129
127
  end
130
128
 
131
129
  # Returns a Hash containing the header for an HTTP request.
@@ -138,7 +136,7 @@ module Savon
138
136
  self.class.logger.send self.class.log_level, message if log?
139
137
  end
140
138
 
141
- # Returns whether logging is possible.
139
+ # Returns whether to log.
142
140
  def log?
143
141
  self.class.log? && self.class.logger.respond_to?(self.class.log_level)
144
142
  end
@@ -28,7 +28,7 @@ module Savon
28
28
 
29
29
  # Returns whether there was a SOAP fault.
30
30
  def soap_fault?
31
- @soap_fault
31
+ @soap_fault ? true : false
32
32
  end
33
33
 
34
34
  # Returns the SOAP fault message.
@@ -36,15 +36,15 @@ module Savon
36
36
 
37
37
  # Returns whether there was an HTTP error.
38
38
  def http_error?
39
- @http_error
39
+ @http_error ? true : false
40
40
  end
41
41
 
42
42
  # Returns the HTTP error message.
43
43
  attr_reader :http_error
44
44
 
45
- # Returns the SOAP response as a Hash.
45
+ # Returns the SOAP response body as a Hash.
46
46
  def to_hash
47
- @body.find_regexp(/.+/).map_soap_response
47
+ @body ||= Crack::XML.parse(@response.body).find_soap_body
48
48
  end
49
49
 
50
50
  # Returns the SOAP response XML.
@@ -52,45 +52,33 @@ module Savon
52
52
  @response.body
53
53
  end
54
54
 
55
- alias :to_s :to_xml
55
+ alias :to_s :to_xml
56
56
 
57
57
  private
58
58
 
59
- # Returns the SOAP response body as a Hash.
60
- def body
61
- unless @body
62
- body = Crack::XML.parse @response.body
63
- @body = body.find_regexp [/.+:Envelope/, /.+:Body/]
64
- end
65
- @body
66
- end
67
-
68
59
  # Handles SOAP faults. Raises a Savon::SOAPFault unless the default
69
60
  # behavior of raising errors was turned off.
70
61
  def handle_soap_fault
71
62
  if soap_fault_message
72
63
  @soap_fault = soap_fault_message
73
- raise Savon::SOAPFault, soap_fault_message if self.class.raise_errors?
64
+ raise Savon::SOAPFault, @soap_fault if self.class.raise_errors?
74
65
  end
75
66
  end
76
67
 
77
68
  # Returns a SOAP fault message in case a SOAP fault was found.
78
69
  def soap_fault_message
79
- unless @soap_fault_message
80
- soap_fault = body.find_regexp [/.+:Fault/]
81
- @soap_fault_message = soap_fault_message_by_version(soap_fault)
82
- end
83
- @soap_fault_message
70
+ @soap_fault_message ||= soap_fault_message_by_version to_hash[:fault]
84
71
  end
85
72
 
86
73
  # Expects a Hash that might contain information about a SOAP fault.
87
74
  # Returns the SOAP fault message in case one was found.
88
75
  def soap_fault_message_by_version(soap_fault)
89
- if soap_fault.keys.include? "faultcode"
90
- "(#{soap_fault['faultcode']}) #{soap_fault['faultstring']}"
91
- elsif soap_fault.keys.include? "Code"
92
- # SOAP 1.2 error code element is capitalized, see: http://www.w3.org/TR/soap12-part1/#faultcodeelement
93
- "(#{soap_fault['Code']['Value']}) #{soap_fault['Reason']['Text']}"
76
+ return unless soap_fault
77
+
78
+ if soap_fault.keys.include? :faultcode
79
+ "(#{soap_fault[:faultcode]}) #{soap_fault[:faultstring]}"
80
+ elsif soap_fault.keys.include? :code
81
+ "(#{soap_fault[:code][:value]}) #{soap_fault[:reason][:text]}"
94
82
  end
95
83
  end
96
84