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.
- data/README.textile +68 -0
- data/Rakefile +9 -7
- data/VERSION +1 -1
- data/lib/savon.rb +33 -34
- data/lib/savon/client.rb +96 -0
- data/lib/savon/core_ext.rb +6 -0
- data/lib/savon/core_ext/datetime.rb +8 -0
- data/lib/savon/core_ext/hash.rb +65 -0
- data/lib/savon/core_ext/object.rb +14 -0
- data/lib/savon/core_ext/string.rb +41 -0
- data/lib/savon/core_ext/symbol.rb +8 -0
- data/lib/savon/core_ext/uri.rb +10 -0
- data/lib/savon/request.rb +103 -0
- data/lib/savon/soap.rb +71 -0
- data/lib/savon/validation.rb +57 -0
- data/lib/savon/wsdl.rb +39 -41
- data/lib/savon/wsse.rb +111 -0
- data/spec/fixtures/multiple_user_response.xml +22 -0
- data/spec/fixtures/soap_fault.xml +0 -0
- data/spec/fixtures/user_fixture.rb +42 -0
- data/spec/fixtures/user_response.xml +4 -2
- data/spec/fixtures/user_wsdl.xml +0 -0
- data/spec/http_stubs.rb +20 -0
- data/spec/savon/client_spec.rb +144 -0
- data/spec/savon/core_ext/datetime_spec.rb +12 -0
- data/spec/savon/core_ext/hash_spec.rb +146 -0
- data/spec/savon/core_ext/object_spec.rb +26 -0
- data/spec/savon/core_ext/string_spec.rb +52 -0
- data/spec/savon/core_ext/symbol_spec.rb +11 -0
- data/spec/savon/core_ext/uri_spec.rb +15 -0
- data/spec/savon/request_spec.rb +93 -0
- data/spec/savon/savon_spec.rb +37 -0
- data/spec/savon/soap_spec.rb +101 -0
- data/spec/savon/validation_spec.rb +88 -0
- data/spec/savon/wsdl_spec.rb +17 -46
- data/spec/savon/wsse_spec.rb +169 -0
- data/spec/spec_helper.rb +7 -92
- data/spec/spec_helper_methods.rb +29 -0
- metadata +68 -20
- data/README.rdoc +0 -62
- data/lib/savon/service.rb +0 -151
- data/spec/savon/service_spec.rb +0 -76
data/README.textile
ADDED
@@ -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.
|
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
|
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("
|
41
|
-
spec.add_runtime_dependency("
|
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("
|
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.
|
1
|
+
0.5.0
|
data/lib/savon.rb
CHANGED
@@ -1,44 +1,43 @@
|
|
1
1
|
module Savon
|
2
2
|
|
3
|
-
#
|
4
|
-
|
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
|
-
#
|
15
|
-
|
6
|
+
# Supported SOAP versions.
|
7
|
+
SOAPVersions = [1, 2]
|
16
8
|
|
17
|
-
#
|
18
|
-
|
19
|
-
@@logger = logger
|
20
|
-
end
|
9
|
+
# SOAP xs:dateTime format.
|
10
|
+
SOAPDateTimeFormat = "%Y-%m-%dT%H:%M:%S"
|
21
11
|
|
22
|
-
#
|
23
|
-
|
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
|
-
#
|
28
|
-
|
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
|
-
|
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
|
-
|
43
|
-
|
44
|
-
|
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"
|
data/lib/savon/client.rb
ADDED
@@ -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,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
|