savon 0.3.2 → 0.5.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 (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