savon 0.6.8 → 0.7.0

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