smacks-savon 0.1.61 → 0.2.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.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