smacks-savon 0.1.61 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +27 -50
- data/Rakefile +48 -0
- data/VERSION +1 -0
- data/lib/savon.rb +47 -4
- data/lib/savon/service.rb +64 -90
- data/lib/savon/wsdl.rb +41 -46
- data/spec/fixtures/soap_fault.xml +8 -0
- data/spec/fixtures/user_response.xml +13 -0
- data/spec/fixtures/user_wsdl.xml +106 -0
- data/spec/savon/service_spec.rb +64 -0
- data/spec/savon/wsdl_spec.rb +65 -0
- data/spec/spec_helper.rb +100 -0
- metadata +47 -20
- data/lib/savon/response.rb +0 -173
- data/test/factories/wsdl.rb +0 -127
- data/test/fixtures/soap_response_fixture.rb +0 -75
- data/test/helper.rb +0 -48
- data/test/savon/test_response.rb +0 -191
- data/test/savon/test_service.rb +0 -35
- data/test/savon/test_wsdl.rb +0 -52
- data/test/test_savon.rb +0 -4
data/README.rdoc
CHANGED
@@ -1,12 +1,8 @@
|
|
1
1
|
= Savon
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
Instantiate a new Savon::Service passing in the URI to the WSDL of the
|
7
|
-
service you would like to use. Then call the SOAP service method on your
|
8
|
-
Savon::Service instance (catched via method_missing) and pass in a Hash
|
9
|
-
of options for the service method to receive.
|
3
|
+
Savon::Service is a SOAP client library to enjoy. The goal is to minimize
|
4
|
+
the overhead of working with SOAP services and provide a lightweight
|
5
|
+
alternative to other libraries.
|
10
6
|
|
11
7
|
== Install
|
12
8
|
|
@@ -14,67 +10,48 @@ of options for the service method to receive.
|
|
14
10
|
|
15
11
|
== Dependencies
|
16
12
|
|
17
|
-
|
18
|
-
|
13
|
+
smacks-apricoteatsgorilla >= 0.5.2
|
14
|
+
hpricot 0.8.241 (the latest JRuby-compatible version)
|
15
|
+
|
16
|
+
Hpricot 0.8.241 is also available at: {Apricot eats Gorilla Downloads}[http://github.com/smacks/apricoteatsgorilla/downloads]
|
19
17
|
|
20
18
|
== How to use
|
21
19
|
|
22
20
|
Instantiate a new Savon::Service instance passing in the WSDL of your service.
|
23
21
|
|
24
|
-
proxy = Savon::Service.new
|
25
|
-
|
26
|
-
Call the SOAP service method of your choice on your Savon::Service instance
|
27
|
-
passing in a Hash of options for the service method to receive.
|
28
|
-
|
29
|
-
response = proxy.findExampleById(:id => 123)
|
30
|
-
|
31
|
-
=== Check for available SOAP service methods
|
32
|
-
|
33
|
-
You can use the service_methods method of the WSDL in your Savon::Service
|
34
|
-
instance to get a list of available SOAP service methods.
|
22
|
+
proxy = Savon::Service.new("http://example.com/ExampleService?wsdl")
|
35
23
|
|
36
|
-
|
37
|
-
# => [ "findExampleById", "findExampleByName" ]
|
24
|
+
Call the SOAP service method of your choice on your Savon::Service instance.
|
38
25
|
|
39
|
-
|
26
|
+
response = proxy.get_all_users
|
40
27
|
|
41
|
-
|
28
|
+
Or pass in a Hash of options for the SOAP service to receive.
|
42
29
|
|
43
|
-
response.
|
44
|
-
response.error?
|
30
|
+
response = proxy.find_user_by_id(:id => 123)
|
45
31
|
|
46
|
-
|
32
|
+
Or specify a custom XPath-Expression to start translating the SOAP response at.
|
47
33
|
|
48
|
-
response.
|
49
|
-
response.error_code
|
34
|
+
response = proxy.find_user_by_id(nil, "//user/email")
|
50
35
|
|
51
|
-
===
|
36
|
+
=== Check for available SOAP actions
|
52
37
|
|
53
|
-
|
54
|
-
object using one of the following methods.
|
38
|
+
Access the WSDL to get an Array of SOAP actions found in the WSDL document.
|
55
39
|
|
56
|
-
|
57
|
-
|
40
|
+
proxy.wsdl.soap_actions
|
41
|
+
# => [ "getAllUsers", "findUserById" ]
|
58
42
|
|
59
|
-
|
60
|
-
response.to_hash
|
43
|
+
=== Handle HTTP error and SOAP faults
|
61
44
|
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
The to_hash and to_mash methods accept an XPath expression (Hpricot search)
|
66
|
-
as second parameter to define a custom root node to start translating the
|
67
|
-
response XML at.
|
68
|
-
|
69
|
-
# response as a Hash starting at a custom root node
|
70
|
-
response.to_hash("//item")
|
71
|
-
|
72
|
-
# response as a Mash starting at a custom root node
|
73
|
-
response.to_mash("//user/email")
|
45
|
+
Savon::Service raises a Savon::SOAPFault in case of a SOAP fault and a
|
46
|
+
Savon::HTTPError in case of an HTTP error.
|
74
47
|
|
75
48
|
=== Logging request and response
|
76
49
|
|
77
|
-
You should specify the logger to use before working with any
|
50
|
+
You should specify the logger to use before working with any service.
|
78
51
|
|
79
52
|
# example for Ruby on Rails
|
80
|
-
Savon
|
53
|
+
Savon.logger = RAILS_DEFAULT_LOGGER
|
54
|
+
|
55
|
+
Of course you can also specify the log level if needed. By default it's set to :debug.
|
56
|
+
|
57
|
+
Savon.log_level = :info
|
data/Rakefile
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
require "rubygems"
|
2
|
+
require "rake"
|
3
|
+
require "spec/rake/spectask"
|
4
|
+
require "rake/rdoctask"
|
5
|
+
|
6
|
+
task :default => :spec
|
7
|
+
|
8
|
+
Spec::Rake::SpecTask.new do |spec|
|
9
|
+
spec.spec_files = FileList["spec/**/*_spec.rb"]
|
10
|
+
spec.spec_opts << "--color"
|
11
|
+
end
|
12
|
+
|
13
|
+
Rake::RDocTask.new do |rdoc|
|
14
|
+
rdoc.title = "Savon"
|
15
|
+
rdoc.rdoc_dir = "rdoc"
|
16
|
+
rdoc.main = "README.rdoc"
|
17
|
+
rdoc.rdoc_files.include("README.rdoc", "lib/**/*.rb")
|
18
|
+
rdoc.options = ["--line-numbers", "--inline-source"]
|
19
|
+
end
|
20
|
+
|
21
|
+
begin
|
22
|
+
require "jeweler"
|
23
|
+
Jeweler::Tasks.new do |spec|
|
24
|
+
spec.name = "savon"
|
25
|
+
spec.author = "Daniel Harrington"
|
26
|
+
spec.email = "me@d-harrington.com"
|
27
|
+
spec.homepage = "http://github.com/smacks/savon"
|
28
|
+
spec.summary = "SOAP client library to enjoy."
|
29
|
+
spec.description = spec.summary
|
30
|
+
|
31
|
+
spec.files = FileList["[A-Z]*", "{lib,spec}/**/*.{rb,xml}"]
|
32
|
+
|
33
|
+
spec.rdoc_options += [
|
34
|
+
"--title", "Savon",
|
35
|
+
"--main", "README.rdoc",
|
36
|
+
"--line-numbers",
|
37
|
+
"--inline-source"
|
38
|
+
]
|
39
|
+
|
40
|
+
spec.add_runtime_dependency("hpricot", "0.8.241")
|
41
|
+
spec.add_runtime_dependency("smacks-apricoteatsgorilla", "0.5.2")
|
42
|
+
|
43
|
+
spec.add_development_dependency("rspec", ">= 1.2.8")
|
44
|
+
spec.add_development_dependency("rr", ">= 0.10.0")
|
45
|
+
end
|
46
|
+
rescue LoadError
|
47
|
+
puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
|
48
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.0
|
data/lib/savon.rb
CHANGED
@@ -1,4 +1,47 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
1
|
+
module Savon
|
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
|
13
|
+
|
14
|
+
# The log level to use.
|
15
|
+
@@log_level = :debug
|
16
|
+
|
17
|
+
# Sets the logger to use.
|
18
|
+
def self.logger=(logger)
|
19
|
+
@@logger = logger
|
20
|
+
end
|
21
|
+
|
22
|
+
# Sets the log level to use.
|
23
|
+
def self.log_level=(log_level)
|
24
|
+
@@log_level = log_level
|
25
|
+
end
|
26
|
+
|
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
|
35
|
+
|
36
|
+
end
|
37
|
+
|
38
|
+
%w(net/http uri rubygems hpricot).each do |gem|
|
39
|
+
require gem
|
40
|
+
end
|
41
|
+
|
42
|
+
#require "apricoteatsgorilla"
|
43
|
+
require File.join(File.dirname(__FILE__), "..", "..", "apricoteatsgorilla", "lib", "apricoteatsgorilla")
|
44
|
+
|
45
|
+
%w(service wsdl).each do |file|
|
46
|
+
require File.join(File.dirname(__FILE__), "savon", file)
|
47
|
+
end
|
data/lib/savon/service.rb
CHANGED
@@ -1,121 +1,95 @@
|
|
1
|
-
require "rubygems"
|
2
|
-
require "net/http"
|
3
|
-
require "uri"
|
4
|
-
require "apricoteatsgorilla"
|
5
|
-
|
6
1
|
module Savon
|
7
2
|
|
8
|
-
# Savon
|
3
|
+
# == Savon::Service
|
4
|
+
#
|
5
|
+
# Savon::Service is a SOAP client library to enjoy. The goal is to minimize
|
6
|
+
# the overhead of working with SOAP services and provide a lightweight
|
7
|
+
# alternative to other libraries.
|
8
|
+
#
|
9
|
+
# ==== Example
|
9
10
|
#
|
10
|
-
#
|
11
|
-
#
|
12
|
-
# service you would like to use. Then call the SOAP service method on your
|
13
|
-
# Savon::Service instance (catched via method_missing) and pass in a Hash
|
14
|
-
# of options for the service method to receive.
|
11
|
+
# proxy = Savon::Service.new("http://example.com/ExampleService?wsdl")
|
12
|
+
# response = proxy.find_user_by_id(:id => 123)
|
15
13
|
class Service
|
16
14
|
|
17
|
-
#
|
18
|
-
@@logger = nil
|
19
|
-
|
20
|
-
# The log level to use.
|
21
|
-
@@log_level = :debug
|
22
|
-
|
23
|
-
# Initializer expects the WSDL +endpoint+ URI and defines nodes to
|
24
|
-
# namespace for Apricot eats Gorilla.
|
15
|
+
# Initializer expects an +endpoint+ URI.
|
25
16
|
def initialize(endpoint)
|
26
|
-
|
17
|
+
raise ArgumentError, "Invalid endpoint: #{endpoint}" unless /^http.+/ === endpoint
|
18
|
+
@endpoint = URI(endpoint)
|
27
19
|
end
|
28
20
|
|
29
|
-
# Returns an instance of
|
21
|
+
# Returns an instance of Savon::WSDL.
|
30
22
|
def wsdl
|
31
|
-
@wsdl
|
32
|
-
@wsdl
|
23
|
+
@wsdl ||= WSDL.new(@endpoint, http)
|
33
24
|
end
|
34
25
|
|
35
|
-
|
36
|
-
def http=(http)
|
37
|
-
@http = http
|
38
|
-
end
|
26
|
+
private
|
39
27
|
|
40
|
-
#
|
41
|
-
|
42
|
-
|
43
|
-
|
28
|
+
# Dispatches a SOAP request, handles any HTTP errors and SOAP faults
|
29
|
+
# and returns the SOAP response.
|
30
|
+
def dispatch(soap_action, soap_body, response_xpath)
|
31
|
+
ApricotEatsGorilla.nodes_to_namespace = { :wsdl => wsdl.choice_elements }
|
32
|
+
headers, body = build_request_parameters(soap_action, soap_body)
|
44
33
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
end
|
34
|
+
Savon.log("SOAP request: #{@endpoint}")
|
35
|
+
Savon.log(headers.map { |k, v| "#{k}: #{v}" }.join(", "))
|
36
|
+
Savon.log(body)
|
49
37
|
|
50
|
-
|
38
|
+
response = http.request_post(@endpoint.path, body, headers)
|
51
39
|
|
52
|
-
|
53
|
-
|
54
|
-
headers = { "Content-Type" => "text/xml; charset=utf-8", "SOAPAction" => @soap_action }
|
40
|
+
Savon.log("SOAP response (status #{response.code})")
|
41
|
+
Savon.log(response.body)
|
55
42
|
|
56
|
-
|
57
|
-
|
58
|
-
|
43
|
+
soap_fault = ApricotEatsGorilla[response.body, "//soap:Fault"]
|
44
|
+
raise_soap_fault(soap_fault) if soap_fault && !soap_fault.empty?
|
45
|
+
raise_http_error(response) if response.code.to_i >= 300
|
59
46
|
|
60
|
-
|
61
|
-
logger.send @@log_level, "Requesting #{@uri}"
|
62
|
-
logger.send @@log_level, headers.map { |key, value| "#{key}: #{value}" }.join("\n")
|
63
|
-
logger.send @@log_level, body
|
64
|
-
end
|
65
|
-
response = http.request_post(@uri.path, body, headers)
|
66
|
-
debug do |logger|
|
67
|
-
logger.send @@log_level, "Response (Status #{response.code}):"
|
68
|
-
logger.send @@log_level, response.body
|
69
|
-
end
|
70
|
-
Savon::Response.new response, root_node
|
47
|
+
ApricotEatsGorilla[response.body, response_xpath]
|
71
48
|
end
|
72
49
|
|
73
|
-
#
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
50
|
+
# Expects the requested +soap_action+ and +soap_body+ and builds and
|
51
|
+
# returns the request header and body to dispatch a SOAP request.
|
52
|
+
def build_request_parameters(soap_action, soap_body)
|
53
|
+
headers = { "Content-Type" => "text/xml; charset=utf-8", "SOAPAction" => soap_action }
|
54
|
+
body = ApricotEatsGorilla.soap_envelope(:wsdl => wsdl.namespace_uri) do
|
55
|
+
ApricotEatsGorilla["wsdl:#{soap_action}" => soap_body]
|
78
56
|
end
|
79
|
-
|
57
|
+
[headers, body]
|
80
58
|
end
|
81
59
|
|
82
|
-
#
|
83
|
-
#
|
84
|
-
def
|
85
|
-
|
86
|
-
raise ArgumentError, "Invalid service method: #{@soap_action}"
|
87
|
-
end
|
60
|
+
# Expects a Hash containing information about a SOAP fault and raises
|
61
|
+
# a Savon::SOAPFault.
|
62
|
+
def raise_soap_fault(soap_fault)
|
63
|
+
raise SOAPFault, "#{soap_fault[:faultcode]}: #{soap_fault[:faultstring]}"
|
88
64
|
end
|
89
65
|
|
90
|
-
#
|
91
|
-
def
|
92
|
-
|
93
|
-
ApricotEatsGorilla.node_namespace = "wsdl"
|
66
|
+
# Expects a Net::HTTPResponse and raises a Savon::HTTPError.
|
67
|
+
def raise_http_error(response)
|
68
|
+
raise HTTPError, "#{response.message} (#{response.code}): #{response.body}"
|
94
69
|
end
|
95
70
|
|
96
|
-
#
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
71
|
+
# Returns a Net::HTTP instance.
|
72
|
+
def http
|
73
|
+
@http ||= Net::HTTP.new(@endpoint.host, @endpoint.port)
|
74
|
+
end
|
75
|
+
|
76
|
+
# Catches calls to SOAP actions, checks if the method called was found in
|
77
|
+
# the WSDL and dispatches the SOAP action if it's valid. Takes an optional
|
78
|
+
# Hash of options to be passed to the SOAP action and an optional XPath-
|
79
|
+
# Expression to define a custom XML root node to start parsing the SOAP
|
80
|
+
# response at.
|
81
|
+
def method_missing(method, *args)
|
82
|
+
soap_action = camelize(method)
|
83
|
+
super unless wsdl.soap_actions.include? soap_action
|
84
|
+
soap_body = args[0] || {}
|
85
|
+
response_xpath = args[1] || "//return"
|
86
|
+
dispatch(soap_action, soap_body, response_xpath)
|
103
87
|
end
|
104
88
|
|
105
|
-
#
|
106
|
-
|
107
|
-
|
108
|
-
# The given +method+ will be validated against available SOAP actions found
|
109
|
-
# on the WSDL and dispatched if available. Options for the SOAP action to
|
110
|
-
# receive can be given through the optional Hash of +options+. A custom
|
111
|
-
# +root_node+ to start parsing the SOAP response at might be supplied as well.
|
112
|
-
def method_missing(method, options = {}, root_node = nil)
|
113
|
-
@soap_action = ApricotEatsGorilla.to_lower_camel_case(method)
|
114
|
-
@options = options
|
115
|
-
validate_soap_action
|
116
|
-
setup_parser
|
117
|
-
dispatch(root_node)
|
89
|
+
# Converts a given +string+ from snake_case to lowerCamelCase.
|
90
|
+
def camelize(string)
|
91
|
+
string.to_s.gsub(/_(.)/) { $1.upcase } if string
|
118
92
|
end
|
119
93
|
|
120
94
|
end
|
121
|
-
end
|
95
|
+
end
|
data/lib/savon/wsdl.rb
CHANGED
@@ -1,75 +1,70 @@
|
|
1
|
-
require "rubygems"
|
2
|
-
require "net/http"
|
3
|
-
require "hpricot"
|
4
|
-
|
5
1
|
module Savon
|
6
2
|
|
7
|
-
# Savon::
|
8
|
-
class
|
3
|
+
# Savon::WSDL represents the WSDL document.
|
4
|
+
class WSDL
|
9
5
|
|
10
|
-
#
|
11
|
-
|
6
|
+
# Returns the namespace URI.
|
7
|
+
def namespace_uri
|
8
|
+
@namespace ||= parse_namespace_uri
|
9
|
+
end
|
12
10
|
|
13
|
-
# SOAP
|
14
|
-
|
11
|
+
# Returns an Array of available SOAP actions.
|
12
|
+
def soap_actions
|
13
|
+
@soap_actions ||= parse_soap_actions
|
14
|
+
end
|
15
15
|
|
16
|
-
#
|
17
|
-
|
16
|
+
# Returns an Array of choice elements.
|
17
|
+
def choice_elements
|
18
|
+
@choice_elements ||= parse_choice_elements
|
19
|
+
end
|
18
20
|
|
19
|
-
# Initializer expects
|
20
|
-
# then gets and parses the WSDL at the given URI.
|
21
|
+
# Initializer expects the endpoint +uri+ and a Net::HTTP instance (+http+).
|
21
22
|
def initialize(uri, http)
|
22
23
|
@uri, @http = uri, http
|
23
|
-
get_wsdl
|
24
|
-
|
25
|
-
parse_namespace_uri
|
26
|
-
parse_service_methods
|
27
|
-
parse_choice_elements
|
28
24
|
end
|
29
25
|
|
30
|
-
# Returns the
|
26
|
+
# Returns the body of the Net::HTTPResponse from the WSDL request.
|
31
27
|
def to_s
|
32
|
-
@response.body
|
28
|
+
@response ? @response.body : nil
|
33
29
|
end
|
34
30
|
|
35
31
|
private
|
36
32
|
|
37
|
-
#
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
raise ArgumentError, "Unable to find WSDL at
|
33
|
+
# Returns an Hpricot::Document of the WSDL. Retrieves the WSDL from the
|
34
|
+
# endpoint URI in case it wasn't retrieved already.
|
35
|
+
def document
|
36
|
+
unless @document
|
37
|
+
@response = @http.get("#{@uri.path}?#{@uri.query}")
|
38
|
+
@document = Hpricot.XML(@response.body)
|
39
|
+
raise ArgumentError, "Unable to find WSDL at: #{@uri}" if
|
40
|
+
!soap_actions || soap_actions.empty?
|
44
41
|
end
|
42
|
+
@document
|
45
43
|
end
|
46
44
|
|
47
45
|
# Parses the WSDL for the namespace URI.
|
48
46
|
def parse_namespace_uri
|
49
|
-
|
50
|
-
|
47
|
+
definitions = document.at("//wsdl:definitions")
|
48
|
+
definitions.get_attribute("targetNamespace") if definitions
|
51
49
|
end
|
52
50
|
|
53
|
-
# Parses the WSDL for available SOAP
|
54
|
-
def
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
end
|
51
|
+
# Parses the WSDL for available SOAP actions.
|
52
|
+
def parse_soap_actions
|
53
|
+
soap_actions = document.search("//soap:operation")
|
54
|
+
|
55
|
+
soap_actions.collect do |soap_action|
|
56
|
+
soap_action.parent.get_attribute("name")
|
57
|
+
end if soap_actions
|
61
58
|
end
|
62
59
|
|
63
60
|
# Parses the WSDL for choice elements.
|
64
61
|
def parse_choice_elements
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
end
|
71
|
-
end
|
62
|
+
choice_elements = document.search("//xs:choice//xs:element")
|
63
|
+
|
64
|
+
choice_elements.collect do |choice_element|
|
65
|
+
choice_element.get_attribute("ref").sub(/(.+):/, "")
|
66
|
+
end if choice_elements
|
72
67
|
end
|
73
68
|
|
74
69
|
end
|
75
|
-
end
|
70
|
+
end
|