savon 0.3.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. data/README.textile +68 -0
  2. data/Rakefile +9 -7
  3. data/VERSION +1 -1
  4. data/lib/savon.rb +33 -34
  5. data/lib/savon/client.rb +96 -0
  6. data/lib/savon/core_ext.rb +6 -0
  7. data/lib/savon/core_ext/datetime.rb +8 -0
  8. data/lib/savon/core_ext/hash.rb +65 -0
  9. data/lib/savon/core_ext/object.rb +14 -0
  10. data/lib/savon/core_ext/string.rb +41 -0
  11. data/lib/savon/core_ext/symbol.rb +8 -0
  12. data/lib/savon/core_ext/uri.rb +10 -0
  13. data/lib/savon/request.rb +103 -0
  14. data/lib/savon/soap.rb +71 -0
  15. data/lib/savon/validation.rb +57 -0
  16. data/lib/savon/wsdl.rb +39 -41
  17. data/lib/savon/wsse.rb +111 -0
  18. data/spec/fixtures/multiple_user_response.xml +22 -0
  19. data/spec/fixtures/soap_fault.xml +0 -0
  20. data/spec/fixtures/user_fixture.rb +42 -0
  21. data/spec/fixtures/user_response.xml +4 -2
  22. data/spec/fixtures/user_wsdl.xml +0 -0
  23. data/spec/http_stubs.rb +20 -0
  24. data/spec/savon/client_spec.rb +144 -0
  25. data/spec/savon/core_ext/datetime_spec.rb +12 -0
  26. data/spec/savon/core_ext/hash_spec.rb +146 -0
  27. data/spec/savon/core_ext/object_spec.rb +26 -0
  28. data/spec/savon/core_ext/string_spec.rb +52 -0
  29. data/spec/savon/core_ext/symbol_spec.rb +11 -0
  30. data/spec/savon/core_ext/uri_spec.rb +15 -0
  31. data/spec/savon/request_spec.rb +93 -0
  32. data/spec/savon/savon_spec.rb +37 -0
  33. data/spec/savon/soap_spec.rb +101 -0
  34. data/spec/savon/validation_spec.rb +88 -0
  35. data/spec/savon/wsdl_spec.rb +17 -46
  36. data/spec/savon/wsse_spec.rb +169 -0
  37. data/spec/spec_helper.rb +7 -92
  38. data/spec/spec_helper_methods.rb +29 -0
  39. metadata +68 -20
  40. data/README.rdoc +0 -62
  41. data/lib/savon/service.rb +0 -151
  42. data/spec/savon/service_spec.rb +0 -76
@@ -0,0 +1,68 @@
1
+ h1. Savon
2
+
3
+ p. Savon can be installed as a gem via:
4
+
5
+ bc. $ gem install savon
6
+
7
+ h3. Dependencies
8
+
9
+ bc. builder >= 2.1.2
10
+ crack >= 0.1.4
11
+
12
+ h2. Warning
13
+
14
+ p. To use a heavy metal library like this, you have to be familiar with SOAP, WSDL and tools like soapUI.
15
+
16
+ h2. First step
17
+
18
+ p. Instantiate a new instance of Savon::Client, passing in the WSDL of your service.
19
+
20
+ bc. proxy = Savon::Client.new "http://example.com/UserService?wsdl"
21
+
22
+ h3. The WSDL
23
+
24
+ p. You can find out about the SOAP actions available on the webservice by using the WSDL object.
25
+
26
+ bc. proxy.wsdl.soap_actions
27
+ => [:get_all_users, :get_user_by_id, :user_magic]
28
+
29
+ p. Find out more about the "WSDL":http://wiki.github.com/rubiii/savon/wsdl object.
30
+
31
+ h3. Calling a SOAP action
32
+
33
+ p. Now, assuming your service applies to the default "Options":http://wiki.github.com/rubiii/savon/options, you can just call any available SOAP action.
34
+
35
+ bc. response = proxy.get_all_users
36
+
37
+ p. Savon lets you call SOAP actions using snake_case, because even though they will propably be written in lowerCamelCase or CamelCase, it just feels much more natural.
38
+
39
+ h3. Parameters
40
+
41
+ p. Specifying parameters for the SOAP service, can be done by simply passing a Hash to the SOAP action.
42
+
43
+ bc. response = proxy.get_user_by_id :id => 666
44
+
45
+ p. Learn more about [[Parameters]].
46
+
47
+ h3. The response
48
+
49
+ p. By default, the SOAP response is translated into a Hash. Take a look at the "Options":http://wiki.github.com/rubiii/savon/options for more information.
50
+
51
+ bc. proxy.get_user_by_id :id => 666
52
+ => { :user_response => { :id => "666", :username => "gorilla" } }
53
+
54
+ h3. HTTP errors and SOAP faults
55
+
56
+ p. Savon raises a Savon::SOAPFault in case of a SOAP fault and a Savon::HTTPError in case of an HTTP error. More information about "Errors":http://wiki.github.com/rubiii/savon/errors.
57
+
58
+ h3. Logging
59
+
60
+ p. By default Savon logs each request and response to STDOUT. Specifying your own logger is as easy as it gets:
61
+
62
+ bc. Savon::Request.logger = RAILS_DEFAULT_LOGGER
63
+
64
+ Read more about "Logging":http://wiki.github.com/rubiii/savon/logging.
65
+
66
+ h3. RDoc and Wiki
67
+
68
+ p. Further information: "Wiki":http://wiki.github.com/rubiii/savon and "RDoc":http://rdoc.info/projects/rubiii/savon
data/Rakefile CHANGED
@@ -8,13 +8,15 @@ task :default => :spec
8
8
  Spec::Rake::SpecTask.new do |spec|
9
9
  spec.spec_files = FileList["spec/**/*_spec.rb"]
10
10
  spec.spec_opts << "--color"
11
+ spec.libs << "lib"
12
+ spec.rcov = true
13
+ spec.rcov_dir = "rcov"
11
14
  end
12
15
 
13
16
  Rake::RDocTask.new do |rdoc|
14
17
  rdoc.title = "Savon"
15
18
  rdoc.rdoc_dir = "rdoc"
16
- rdoc.main = "README.rdoc"
17
- rdoc.rdoc_files.include("README.rdoc", "lib/**/*.rb")
19
+ rdoc.rdoc_files.include("lib/**/*.rb")
18
20
  rdoc.options = ["--line-numbers", "--inline-source"]
19
21
  end
20
22
 
@@ -25,23 +27,23 @@ begin
25
27
  spec.author = "Daniel Harrington"
26
28
  spec.email = "me@rubiii.com"
27
29
  spec.homepage = "http://github.com/rubiii/savon"
28
- spec.summary = "SOAP client library to enjoy"
30
+ spec.summary = "Heavy metal Ruby SOAP client library"
29
31
  spec.description = spec.summary
30
32
 
31
33
  spec.files = FileList["[A-Z]*", "{lib,spec}/**/*.{rb,xml}"]
32
34
 
33
35
  spec.rdoc_options += [
34
36
  "--title", "Savon",
35
- "--main", "README.rdoc",
36
37
  "--line-numbers",
37
38
  "--inline-source"
38
39
  ]
39
40
 
40
- spec.add_runtime_dependency("hpricot", "0.8.241")
41
- spec.add_runtime_dependency("apricoteatsgorilla", "0.5.13")
41
+ spec.add_runtime_dependency("builder", ">= 2.1.2")
42
+ spec.add_runtime_dependency("crack", ">= 0.1.4")
42
43
 
43
44
  spec.add_development_dependency("rspec", ">= 1.2.8")
44
- spec.add_development_dependency("rr", ">= 0.10.0")
45
+ spec.add_development_dependency("mocha", ">= 0.9.7")
46
+ spec.add_development_dependency("fakeweb", ">= 1.2.7")
45
47
  end
46
48
  rescue LoadError
47
49
  puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.3.2
1
+ 0.5.0
@@ -1,44 +1,43 @@
1
1
  module Savon
2
2
 
3
- # Raised by the <tt>on_http_error</tt> method in case of an HTTP error.
4
- # <tt>on_http_error</tt> may be overwritten to customize error handling.
5
- class HTTPError < StandardError; end
6
-
7
- # Raised by the <tt>on_soap_fault</tt> method in case of a SOAP fault.
8
- # <tt>on_soap_fault</tt> may be overwritten to customize error handling.
9
- class SOAPFault < StandardError; end
10
-
11
- # The logger to use.
12
- @@logger = nil
3
+ # Current version.
4
+ VERSION = "0.5.0"
13
5
 
14
- # The log level to use.
15
- @@log_level = :debug
6
+ # Supported SOAP versions.
7
+ SOAPVersions = [1, 2]
16
8
 
17
- # Sets the logger to use.
18
- def self.logger=(logger)
19
- @@logger = logger
20
- end
9
+ # SOAP xs:dateTime format.
10
+ SOAPDateTimeFormat = "%Y-%m-%dT%H:%M:%S"
21
11
 
22
- # Sets the log level to use.
23
- def self.log_level=(log_level)
24
- @@log_level = log_level
25
- end
12
+ # SOAP xs:dateTime Regexp.
13
+ SOAPDateTimeRegexp = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/
26
14
 
27
- # Logs a given +message+ using the +@@logger+ instance or yields the logger
28
- # to a given +block+ for logging multiple messages at once.
29
- def self.log(message = nil)
30
- if @@logger
31
- @@logger.send(@@log_level, message) if message
32
- yield @@logger if block_given?
33
- end
34
- end
15
+ # Raised in case of an HTTP error.
16
+ class HTTPError < StandardError; end
35
17
 
36
- end
18
+ # Raised in case of a SOAP fault.
19
+ class SOAPFault < StandardError; end
37
20
 
38
- %w(net/http net/https uri rubygems hpricot apricoteatsgorilla).each do |gem|
39
- require gem
40
21
  end
41
22
 
42
- %w(service wsdl).each do |file|
43
- require File.join(File.dirname(__FILE__), "savon", file)
44
- end
23
+ # stdlib
24
+ require "logger"
25
+ require "net/http"
26
+ require "net/https"
27
+ require "uri"
28
+ require "base64"
29
+ require "digest/sha1"
30
+ require "rexml/document"
31
+
32
+ # gems
33
+ require "builder"
34
+ require "crack/xml"
35
+
36
+ # savon
37
+ require "savon/core_ext"
38
+ require "savon/validation"
39
+ require "savon/wsse"
40
+ require "savon/soap"
41
+ require "savon/request"
42
+ require "savon/wsdl"
43
+ require "savon/client"
@@ -0,0 +1,96 @@
1
+ module Savon
2
+
3
+ # == Savon::Client
4
+ #
5
+ # Heavy metal Ruby SOAP client library. Minimizes the overhead of working
6
+ # with SOAP services and XML.
7
+ class Client
8
+ include Validation
9
+
10
+ # Default behavior for processing the SOAP response. Expects an instance
11
+ # of Net::HTTPResponse and returns the SOAP response body as a Hash.
12
+ @response_process = lambda do |response|
13
+ response_hash = Crack::XML.parse response.body
14
+ error_handling.call response, response_hash
15
+
16
+ response_hash = response_hash["soap:Envelope"]["soap:Body"]
17
+ response_hash = response_hash[response_hash.keys.first]
18
+ (response_hash["return"] || response_hash).map_soap_response
19
+ end
20
+
21
+ # Default error handling. Expects an instance of Net::HTTPResponse and
22
+ # a SOAP response body Hash. Raises a Savon::SOAPFault in case of a SOAP
23
+ # fault or a Savon::HTTPError in case of an HTTP error.
24
+ @error_handling = lambda do |response, response_hash|
25
+ soap_fault = response_hash.to_soap_fault_message
26
+ raise Savon::SOAPFault, soap_fault if soap_fault
27
+
28
+ http_error = "#{response.message} (#{response.code})"
29
+ http_error += ": #{response.body}" unless response.body.empty?
30
+ raise Savon::HTTPError, http_error if response.code.to_i >= 300
31
+ end
32
+
33
+ class << self
34
+
35
+ # Accessor for the default response block.
36
+ attr_accessor :response_process
37
+
38
+ # Accessor for the default error handling.
39
+ attr_accessor :error_handling
40
+
41
+ end
42
+
43
+ # Expects a SOAP +endpoint+ String.
44
+ def initialize(endpoint)
45
+ @request = Request.new endpoint
46
+ @wsdl = WSDL.new @request
47
+ end
48
+
49
+ # Returns the Savon::WSDL.
50
+ attr_reader :wsdl
51
+
52
+ # Returns the Net::HTTPResponse of the last SOAP request.
53
+ attr_reader :response
54
+
55
+ # Behaves as usual, but also returns +true+ for available SOAP actions.
56
+ def respond_to?(method)
57
+ return true if @wsdl.soap_actions.include? method
58
+ super
59
+ end
60
+
61
+ private
62
+
63
+ # Behaves as usual, but dispatches SOAP requests to SOAP actions matching
64
+ # the given +method+ name.
65
+ def method_missing(method, *args, &block) #:doc:
66
+ soap_action = @wsdl.mapped_soap_actions[method]
67
+ super unless soap_action
68
+
69
+ soap_body, options = args[0] || {}, args[1] || {}
70
+ validate_arguments! soap_body, options, block
71
+ dispatch soap_action, soap_body, options, block
72
+ end
73
+
74
+ # Dispatches a given +soap_action+ with a given +soap_body+, +options+
75
+ # and a +block+.
76
+ def dispatch(soap_action, soap_body, options, block = nil)
77
+ @soap = SOAP.new soap_action, soap_body, options, @wsdl.namespace_uri
78
+ @response = @request.soap @soap
79
+ response_process(block).call @response
80
+ end
81
+
82
+ # Returns the response process to use.
83
+ def response_process(block)
84
+ block || self.class.response_process
85
+ end
86
+
87
+ # Validates the given +soap_body+, +options+ and +response_process+.
88
+ def validate_arguments!(soap_body, options = {}, response_process = nil)
89
+ validate! :soap_body, soap_body if soap_body
90
+ validate! :response_process, response_process if response_process
91
+ validate! :soap_version, options[:soap_version] if options[:soap_version]
92
+ validate! :wsse_credentials, options[:wsse] if options[:wsse].kind_of? Hash
93
+ end
94
+
95
+ end
96
+ end
@@ -0,0 +1,6 @@
1
+ require "savon/core_ext/object"
2
+ require "savon/core_ext/string"
3
+ require "savon/core_ext/symbol"
4
+ require "savon/core_ext/datetime"
5
+ require "savon/core_ext/hash"
6
+ require "savon/core_ext/uri"
@@ -0,0 +1,8 @@
1
+ class DateTime
2
+
3
+ # Returns the DateTime as an xs:dateTime formatted String.
4
+ def to_soap_value
5
+ strftime Savon::SOAPDateTimeFormat
6
+ end
7
+
8
+ end
@@ -0,0 +1,65 @@
1
+ class Hash
2
+
3
+ # Returns the Hash translated into SOAP request compatible XML.
4
+ #
5
+ # === Example
6
+ #
7
+ # { :find_user => { :id => 666 } }.to_soap_xml
8
+ # => "<findUser><id>666</id></findUser>"
9
+ def to_soap_xml
10
+ @soap_xml = Builder::XmlMarkup.new
11
+ each { |key, value| nested_data_to_soap_xml key, value }
12
+ @soap_xml.target!
13
+ end
14
+
15
+ # Tries to generate a SOAP fault message from the Hash. Returns nil in
16
+ # case no SOAP fault could be found or generated.
17
+ def to_soap_fault_message
18
+ soap_fault = self["soap:Envelope"]["soap:Body"]["soap:Fault"] rescue {}
19
+ return nil unless soap_fault
20
+
21
+ if soap_fault.keys.include? "faultcode"
22
+ "(#{soap_fault['faultcode']}) #{soap_fault['faultstring']}"
23
+ elsif soap_fault.keys.include? "code"
24
+ "(#{soap_fault['code']['value']}) #{soap_fault['reason']['text']}"
25
+ else
26
+ nil
27
+ end
28
+ end
29
+
30
+ # Maps keys and values of a Hash created from SOAP response XML to
31
+ # more convenient Ruby Objects.
32
+ def map_soap_response
33
+ inject({}) do |hash, (key, value)|
34
+ key = key.strip_namespace.snakecase.to_sym
35
+
36
+ value = case value
37
+ when Hash
38
+ value["xsi:nil"] ? nil : value.map_soap_response
39
+ when Array
40
+ value.map { |a_value| a_value.map_soap_response rescue a_value }
41
+ when String
42
+ value.map_soap_response
43
+ end
44
+ hash.merge key => value
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ # Expects a Hash +key+ and +value+ and recursively creates an XML structure
51
+ # representing the Hash content.
52
+ def nested_data_to_soap_xml(key, value)
53
+ case value
54
+ when Array
55
+ value.map { |sitem| nested_data_to_soap_xml key, sitem }
56
+ when Hash
57
+ @soap_xml.tag!(key.to_soap_key) do
58
+ value.each { |subkey, subvalue| nested_data_to_soap_xml subkey, subvalue }
59
+ end
60
+ else
61
+ @soap_xml.tag!(key.to_soap_key) { @soap_xml << value.to_soap_value }
62
+ end
63
+ end
64
+
65
+ end
@@ -0,0 +1,14 @@
1
+ class Object
2
+
3
+ # Returns the Object as a SOAP request compliant key.
4
+ def to_soap_key
5
+ to_s
6
+ end
7
+
8
+ # Returns the Object as a SOAP request compliant value.
9
+ def to_soap_value
10
+ return to_s unless respond_to? :to_datetime
11
+ to_datetime.to_soap_value
12
+ end
13
+
14
+ end
@@ -0,0 +1,41 @@
1
+ class String
2
+
3
+ # Returns the String in snake_case.
4
+ def snakecase
5
+ str = dup
6
+ str.gsub! /::/, '/'
7
+ str.gsub! /([A-Z]+)([A-Z][a-z])/, '\1_\2'
8
+ str.gsub! /([a-z\d])([A-Z])/, '\1_\2'
9
+ str.tr! "-", "_"
10
+ str.downcase!
11
+ str
12
+ end
13
+
14
+ # Returns the String in lowerCamelCase.
15
+ def lower_camelcase
16
+ str = dup
17
+ str.gsub!(/\/(.?)/) { "::#{$1.upcase}" }
18
+ str.gsub!(/(?:_+|-+)([a-z])/) { $1.upcase }
19
+ str.gsub!(/(\A|\s)([A-Z])/) { $1 + $2.downcase }
20
+ str
21
+ end
22
+
23
+ # Returns the String without namespace.
24
+ def strip_namespace
25
+ gsub /(.+:)(.+)/, '\2'
26
+ end
27
+
28
+ # Translates SOAP response values to more convenient Ruby Objects.
29
+ def map_soap_response
30
+ return DateTime.parse self if Savon::SOAPDateTimeRegexp === self
31
+ return true if self.strip.downcase == "true"
32
+ return false if self.strip.downcase == "false"
33
+ self
34
+ end
35
+
36
+ # Returns the String as a SOAP request compliant value.
37
+ def to_soap_value
38
+ to_s
39
+ end
40
+
41
+ end
@@ -0,0 +1,8 @@
1
+ class Symbol
2
+
3
+ # Returns the Symbol as a lowerCamelCase String.
4
+ def to_soap_key
5
+ to_s.lower_camelcase
6
+ end
7
+
8
+ end
@@ -0,0 +1,10 @@
1
+ module URI
2
+ class HTTP
3
+
4
+ # Returns whether the URI hints to SSL.
5
+ def ssl?
6
+ /^https/ === @scheme
7
+ end
8
+
9
+ end
10
+ end