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 CHANGED
@@ -1,12 +1,8 @@
1
1
  = Savon
2
2
 
3
- Ruby SOAP client library to enjoy.
4
-
5
- Communicating with a SOAP webservice can be done in two lines of code.
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
- hpricot 0.6.164 (also available for JRuby)
18
- smacks-apricoteatsgorilla >= 0.4.1
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 "http://example.com/ExampleService?wsdl"
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
- proxy.wsdl.service_methods
37
- # => [ "findExampleById", "findExampleByName" ]
24
+ Call the SOAP service method of your choice on your Savon::Service instance.
38
25
 
39
- === Handle HTTP and SOAP errors
26
+ response = proxy.get_all_users
40
27
 
41
- Check if the SOAP request was successful.
28
+ Or pass in a Hash of options for the SOAP service to receive.
42
29
 
43
- response.success?
44
- response.error?
30
+ response = proxy.find_user_by_id(:id => 123)
45
31
 
46
- Access error message and code in case a request was not successful.
32
+ Or specify a custom XPath-Expression to start translating the SOAP response at.
47
33
 
48
- response.error_message
49
- response.error_code
34
+ response = proxy.find_user_by_id(nil, "//user/email")
50
35
 
51
- === Different response formats
36
+ === Check for available SOAP actions
52
37
 
53
- To work with the response of the service you need to convert the response
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
- # SOAP response XML:
57
- response.to_s
40
+ proxy.wsdl.soap_actions
41
+ # => [ "getAllUsers", "findUserById" ]
58
42
 
59
- # response as a Hash
60
- response.to_hash
43
+ === Handle HTTP error and SOAP faults
61
44
 
62
- # response as a Mash
63
- response.to_mash
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 services.
50
+ You should specify the logger to use before working with any service.
78
51
 
79
52
  # example for Ruby on Rails
80
- Savon::Service.logger = RAILS_DEFAULT_LOGGER
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
- $:.unshift(File.join(File.dirname(__FILE__), "savon"))
2
- require "service"
3
- require "wsdl"
4
- require "response"
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 - Ruby SOAP client library to enjoy.
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
- # Communicating with a SOAP webservice can be done in two lines of code.
11
- # Instantiate a new Savon::Service passing in the URI to the WSDL of the
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
- # The logger instance to use.
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
- @uri = URI(endpoint)
17
+ raise ArgumentError, "Invalid endpoint: #{endpoint}" unless /^http.+/ === endpoint
18
+ @endpoint = URI(endpoint)
27
19
  end
28
20
 
29
- # Returns an instance of the Savon::Wsdl.
21
+ # Returns an instance of Savon::WSDL.
30
22
  def wsdl
31
- @wsdl = Savon::Wsdl.new(@uri, http) unless @wsdl
32
- @wsdl
23
+ @wsdl ||= WSDL.new(@endpoint, http)
33
24
  end
34
25
 
35
- # Sets the Net::HTTP instance to use.
36
- def http=(http)
37
- @http = http
38
- end
26
+ private
39
27
 
40
- # Sets the logger instance to use.
41
- def self.logger=(logger)
42
- @@logger = logger
43
- end
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
- # Sets the log level to use.
46
- def self.log_level=(log_level)
47
- @@log_level = log_level
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
- private
38
+ response = http.request_post(@endpoint.path, body, headers)
51
39
 
52
- # Constructs and dispatches the SOAP request. Returns a Savon::Response.
53
- def dispatch(root_node = nil)
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
- body = ApricotEatsGorilla.soap_envelope("wsdl" => wsdl.namespace_uri) do
57
- ApricotEatsGorilla["wsdl:#{@soap_action}" => @options]
58
- end
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
- debug do |logger|
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
- # Returns the Net::HTTP instance to use.
74
- def http
75
- if @http.nil?
76
- raise ArgumentError, "Invalid endpoint URI: #{@uri}" unless @uri.scheme
77
- @http = Net::HTTP.new(@uri.host, @uri.port)
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
- @http
57
+ [headers, body]
80
58
  end
81
59
 
82
- # Checks if the requested SOAP action was found on the WSDL.
83
- # Raises an ArgumentError in case it was not found.
84
- def validate_soap_action
85
- unless wsdl.service_methods.include? @soap_action
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
- # Sets options for the XML parser.
91
- def setup_parser
92
- ApricotEatsGorilla.nodes_to_namespace = wsdl.choice_elements
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
- # Logs a given +message+ using the +@@logger+ instance or yields the logger
97
- # to a given +block+ for logging multiple messages at once.
98
- def debug(message = nil)
99
- if @@logger
100
- @@logger.send(@@log_level, message) if message
101
- yield @@logger if block_given?
102
- end
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
- # Catches SOAP actions called on the Savon::Service instance.
106
- # This is the default way of calling a SOAP action.
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::Wsdl represents the WSDL.
8
- class Wsdl
3
+ # Savon::WSDL represents the WSDL document.
4
+ class WSDL
9
5
 
10
- # The namespace URI.
11
- attr_reader :namespace_uri
6
+ # Returns the namespace URI.
7
+ def namespace_uri
8
+ @namespace ||= parse_namespace_uri
9
+ end
12
10
 
13
- # SOAP service methods.
14
- attr_reader :service_methods
11
+ # Returns an Array of available SOAP actions.
12
+ def soap_actions
13
+ @soap_actions ||= parse_soap_actions
14
+ end
15
15
 
16
- # Choice elements.
17
- attr_reader :choice_elements
16
+ # Returns an Array of choice elements.
17
+ def choice_elements
18
+ @choice_elements ||= parse_choice_elements
19
+ end
18
20
 
19
- # Initializer expects an endpoint +uri+ and an +http+ connection instance,
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 response body from the WSDL request.
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
- # Gets the WSDL at the given URI.
38
- def get_wsdl
39
- @response = @http.get("#{@uri.path}?#{@uri.query}")
40
- @doc = Hpricot.XML(@response.body)
41
-
42
- if !@doc.at("//wsdl:definitions")
43
- raise ArgumentError, "Unable to find WSDL at given endpoint URI."
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
- node = @doc.at("//wsdl:definitions")
50
- @namespace_uri = node.get_attribute("targetNamespace") if node
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 service methods.
54
- def parse_service_methods
55
- @service_methods, node = [], @doc.search("//soap:operation")
56
- if node
57
- node.each do |operation|
58
- service_methods << operation.parent.get_attribute("name")
59
- end
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
- @choice_elements, node = [], @doc.search("//xs:choice//xs:element")
66
- if node
67
- node.each do |choice|
68
- name = choice.get_attribute("ref").sub(/(.+):/, "")
69
- choice_elements << name unless @choice_elements.include? name
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