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,103 @@
1
+ module Savon
2
+ class Request
3
+ include Validation
4
+
5
+ # Content-Types by SOAP version.
6
+ ContentType = { 1 => "text/xml", 2 => "application/soap+xml" }
7
+
8
+ # Defines whether to log HTTP requests.
9
+ @log = true
10
+
11
+ # The default logger.
12
+ @logger = Logger.new STDOUT
13
+
14
+ # The default log level.
15
+ @log_level = :debug
16
+
17
+ class << self
18
+ # Sets whether to log HTTP requests.
19
+ attr_writer :log
20
+
21
+ # Returns whether to log HTTP requests.
22
+ def log?
23
+ @log
24
+ end
25
+
26
+ # Accessor for the default logger.
27
+ attr_accessor :logger
28
+
29
+ # Accessor for the default log level.
30
+ attr_accessor :log_level
31
+ end
32
+
33
+ # Expects an endpoint String.
34
+ def initialize(endpoint)
35
+ validate! :endpoint, endpoint
36
+ @endpoint = URI endpoint
37
+ end
38
+
39
+ # Returns the endpoint URI.
40
+ attr_reader :endpoint
41
+
42
+ # Retrieves WSDL document and returns the Net::HTTPResponse.
43
+ def wsdl
44
+ log "Retrieving WSDL from: #{@endpoint}"
45
+ http.get @endpoint.to_s
46
+ end
47
+
48
+ # Executes a SOAP request using a given Savon::SOAP (+soap+) instance
49
+ # and returns the Net::HTTPResponse.
50
+ def soap(soap)
51
+ @soap = soap
52
+ soap_request
53
+ end
54
+
55
+ private
56
+
57
+ # Logs the SOAP request.
58
+ def log_request
59
+ log "SOAP request: #{@endpoint}"
60
+ log http_header.map { |key, value| "#{key}: #{value}" }.join ", "
61
+ log @soap.body
62
+ end
63
+
64
+ # Executes a SOAP request and returns the Net::HTTPResponse.
65
+ def soap_request
66
+ log_request
67
+ @response = http.request_post @endpoint.path, @soap.body, http_header
68
+ log_response
69
+ @response
70
+ end
71
+
72
+ # Logs the SOAP response.
73
+ def log_response
74
+ log "SOAP response (status #{@response.code}):"
75
+ log @response.body
76
+ end
77
+
78
+ # Returns a Net::HTTP instance.
79
+ def http
80
+ unless @http
81
+ @http ||= Net::HTTP.new @endpoint.host, @endpoint.port
82
+ @http.use_ssl = true if @endpoint.ssl?
83
+ end
84
+ @http
85
+ end
86
+
87
+ # Returns a Hash containing the header for an HTTP request.
88
+ def http_header
89
+ { "Content-Type" => ContentType[@soap.version], "SOAPAction" => @soap.action }
90
+ end
91
+
92
+ # Logs a given +message+.
93
+ def log(message)
94
+ self.class.logger.send self.class.log_level, message if log?
95
+ end
96
+
97
+ # Returns whether logging is possible.
98
+ def log?
99
+ self.class.log? && self.class.logger.respond_to?(self.class.log_level)
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,71 @@
1
+ module Savon
2
+ class SOAP
3
+ include WSSE
4
+
5
+ # SOAP namespaces by SOAP version.
6
+ SOAPNamespace = {
7
+ 1 => "http://schemas.xmlsoap.org/soap/envelope/",
8
+ 2 => "http://www.w3.org/2003/05/soap-envelope"
9
+ }
10
+
11
+ # Content-Types by SOAP version.
12
+ ContentType = { 1 => "text/xml", 2 => "application/soap+xml" }
13
+
14
+ # The default SOAP version.
15
+ @version = 1
16
+
17
+ class << self
18
+
19
+ # Accessor for the default SOAP version.
20
+ attr_accessor :version
21
+
22
+ end
23
+
24
+ # Expects a SOAP +action+, +body+, +options+ and the +namespace_uri+.
25
+ def initialize(action, body, options, namespace_uri)
26
+ @action, @body = action, body
27
+ @options, @namespace_uri = options, namespace_uri
28
+ end
29
+
30
+ # Returns the SOAP action.
31
+ attr_reader :action
32
+
33
+ # Returns the SOAP options.
34
+ attr_reader :options
35
+
36
+ # Returns the XML for a SOAP request.
37
+ def body
38
+ builder = Builder::XmlMarkup.new
39
+
40
+ @xml_body ||= builder.env :Envelope, envelope_namespaces do |xml|
41
+ xml.env(:Header) { envelope_header xml }
42
+ xml.env(:Body) { envelope_body xml }
43
+ end
44
+ end
45
+
46
+ # Returns the SOAP version to use.
47
+ def version
48
+ @options[:soap_version] || self.class.version
49
+ end
50
+
51
+ private
52
+
53
+ # Returns a Hash of namespaces for the SOAP envelope.
54
+ def envelope_namespaces
55
+ { "xmlns:env" => SOAPNamespace[version], "xmlns:wsdl" => @namespace_uri }
56
+ end
57
+
58
+ # Expects an instance of Builder::XmlMarkup and returns the XML for the
59
+ # SOAP envelope header.
60
+ def envelope_header(xml)
61
+ wsse_header xml if wsse?
62
+ end
63
+
64
+ # Expects an instance of Builder::XmlMarkup and returns the XML for the
65
+ # SOAP envelope body.
66
+ def envelope_body(xml)
67
+ xml.wsdl(@action.to_sym) { xml << (@body.to_soap_xml rescue @body.to_s) }
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,57 @@
1
+ module Savon
2
+ module Validation
3
+
4
+ # Validates a given +value+ of a given +type+. Raises an ArgumentError
5
+ # in case the value is not valid.
6
+ def validate!(type, value)
7
+ case type
8
+ when :endpoint then validate_endpoint value
9
+ when :soap_version then validate_soap_version value
10
+ when :soap_body then validate_soap_body value
11
+ when :response_process then validate_response_process value
12
+ when :wsse_credentials then validate_wsse_credentials value
13
+ end
14
+ true
15
+ end
16
+
17
+ private
18
+
19
+ # Validates a given +endpoint+.
20
+ def validate_endpoint(endpoint)
21
+ invalid :endpoint, endpoint unless /^(http|https):\/\// === endpoint
22
+ end
23
+
24
+ # Validates a given +soap_version+.
25
+ def validate_soap_version(soap_version)
26
+ invalid :soap_version, soap_version unless SOAPVersions.include? soap_version
27
+ end
28
+
29
+ # Validates a given +soap_body+.
30
+ def validate_soap_body(soap_body)
31
+ invalid :soap_body, soap_body unless
32
+ soap_body.kind_of?(Hash) || soap_body.respond_to?(:to_s)
33
+ end
34
+
35
+ # Validates a given +response_process+.
36
+ def validate_response_process(response_process)
37
+ invalid :response_process, response_process unless
38
+ response_process.respond_to? :call
39
+ end
40
+
41
+ # Validates a given Hash of +wsse_credentials+.
42
+ def validate_wsse_credentials(wsse)
43
+ invalid :wsse_credentials unless wsse[:username] && wsse[:password]
44
+ invalid :wsse_username, wsse[:username] unless wsse[:username].respond_to? :to_s
45
+ invalid :wsse_password, wsse[:password] unless wsse[:password].respond_to? :to_s
46
+ end
47
+
48
+ # Raises an ArgumentError for a given +argument+. Also accepts the invalid
49
+ # +value+ and adds it to the error message.
50
+ def invalid(argument, value = nil)
51
+ message = "Invalid argument '#{argument}'"
52
+ message << ": #{value}" if value
53
+ raise ArgumentError, message
54
+ end
55
+
56
+ end
57
+ end
@@ -1,70 +1,68 @@
1
1
  module Savon
2
2
 
3
- # Savon::WSDL represents the WSDL document.
3
+ # Savon::WSDL
4
+ #
5
+ # Savon::WSDL represents a WSDL document. A WSDL document serves as a more
6
+ # or less qualitative API documentation.
4
7
  class WSDL
8
+ include Validation
5
9
 
6
- # Returns the namespace URI.
7
- def namespace_uri
8
- @namespace ||= parse_namespace_uri
10
+ # Expects a Savon::Request object.
11
+ def initialize(request)
12
+ @request = request
9
13
  end
10
14
 
11
- # Returns an Array of available SOAP actions.
12
- def soap_actions
13
- @soap_actions ||= parse_soap_actions
15
+ # Returns the namespace URI from the WSDL.
16
+ def namespace_uri
17
+ @namespace_uri ||= parse_namespace_uri
14
18
  end
15
19
 
16
- # Returns an Array of choice elements.
17
- def choice_elements
18
- @choice_elements ||= parse_choice_elements
20
+ # Returns an Array of available SOAP actions from the WSDL.
21
+ def soap_actions
22
+ mapped_soap_actions.keys
19
23
  end
20
24
 
21
- # Initializer expects the endpoint +uri+ and a Net::HTTP instance (+http+).
22
- def initialize(uri, http)
23
- @uri, @http = uri, http
25
+ # Returns a Hash of available SOAP actions and their original names.
26
+ def mapped_soap_actions
27
+ @mapped_soap_actions ||= parse_soap_actions.inject Hash.new do |hash, soap_action|
28
+ hash.merge soap_action.snakecase.to_sym => soap_action
29
+ end
24
30
  end
25
31
 
26
- # Returns the body of the Net::HTTPResponse from the WSDL request.
32
+ # Returns the WSDL or +nil+ in case the WSDL could not be retrieved.
27
33
  def to_s
28
- @response ? @response.body : nil
34
+ wsdl_response ? wsdl_response.body : nil
29
35
  end
30
36
 
31
37
  private
32
38
 
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?
39
+ # Retrieves and returns the WSDL response. Raises an ArgumentError in
40
+ # case the WSDL seems to be invalid.
41
+ def wsdl_response
42
+ unless @wsdl_response
43
+ @wsdl_response ||= @request.wsdl
44
+ invalid! :wsdl, @request.endpoint unless soap_actions && !soap_actions.empty?
41
45
  end
42
- @document
46
+ @wsdl_response
47
+ end
48
+
49
+ # Returns a REXML::Document of the WSDL.
50
+ def document
51
+ @document ||= REXML::Document.new wsdl_response.body
43
52
  end
44
53
 
45
54
  # Parses the WSDL for the namespace URI.
46
55
  def parse_namespace_uri
47
- definitions = document.at("//wsdl:definitions")
48
- definitions.get_attribute("targetNamespace") if definitions
56
+ definitions = document.elements["//wsdl:definitions"]
57
+ definitions.attributes["targetNamespace"] if definitions
49
58
  end
50
59
 
51
60
  # Parses the WSDL for available SOAP actions.
52
61
  def parse_soap_actions
53
- soap_actions = document.search("[@soapAction]")
54
-
55
- soap_actions.collect do |soap_action|
56
- soap_action.parent.get_attribute("name")
57
- end if soap_actions
58
- end
59
-
60
- # Parses the WSDL for choice elements.
61
- def parse_choice_elements
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
62
+ document.elements.collect "//[@soapAction]" do |element|
63
+ element.parent.attributes["name"]
64
+ end
67
65
  end
68
66
 
69
67
  end
70
- end
68
+ end
@@ -0,0 +1,111 @@
1
+ module Savon
2
+
3
+ # Savon::WSSE
4
+ #
5
+ # Includes support methods for adding WSSE authentication to a SOAP request.
6
+ module WSSE
7
+
8
+ # Namespace for WS Security Secext.
9
+ WSENamespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
10
+
11
+ # Namespace for WS Security Utility.
12
+ WSUNamespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
13
+
14
+ # Default WSSE username.
15
+ @username = nil
16
+
17
+ # Default WSSE password.
18
+ @password = nil
19
+
20
+ # Default for whether to use WSSE digest.
21
+ @digest = false
22
+
23
+ class << self
24
+
25
+ # Accessor for the default WSSE username.
26
+ attr_accessor :username
27
+
28
+ # Accessor for the default WSSE password.
29
+ attr_accessor :password
30
+
31
+ # Sets whether to use WSSE digest by default.
32
+ attr_writer :digest
33
+
34
+ # Returns whether to use WSSE digest by default.
35
+ def digest?
36
+ @digest
37
+ end
38
+
39
+ end
40
+
41
+ # Returns whether WSSE authentication was set via options.
42
+ def wsse?
43
+ options[:wsse] = {} unless options[:wsse].kind_of? Hash
44
+ wsse_username && wsse_password
45
+ end
46
+
47
+ # Takes a Builder::XmlMarkup instance and appends a WSSE header.
48
+ def wsse_header(xml)
49
+ options[:wsse] = {} unless options[:wsse].kind_of? Hash
50
+
51
+ xml.wsse :Security, "xmlns:wsse" => WSENamespace do
52
+ xml.wsse :UsernameToken, "xmlns:wsu" => WSUNamespace do
53
+ wsse_nodes xml
54
+ end
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ # Returns the WSSE username or false in case no username was found.
61
+ def wsse_username
62
+ username = options[:wsse][:username] || WSSE.username
63
+ username ? username : false
64
+ end
65
+
66
+ # Returns the WSSE password or false in case no password was found.
67
+ def wsse_password
68
+ password = options[:wsse][:password] || WSSE.password
69
+ password ? password : false
70
+ end
71
+
72
+ # Returns whether to use WSSE digest authentication based on options.
73
+ def digest?
74
+ digest = options[:wsse][:digest] || WSSE.digest?
75
+ digest ? true : false
76
+ end
77
+
78
+ # Takes a Builder::XmlMarkup instance and appends the credentials for
79
+ # WSSE authentication.
80
+ def wsse_nodes(xml)
81
+ xml.wsse :Username, wsse_username
82
+ xml.wsse :Nonce, wsse_nonce
83
+ xml.wsu :Created, wsse_timestamp
84
+ xml.wsse :Password, wsse_password_node
85
+ end
86
+
87
+ # Returns the WSSE password. Encrypts the password for digest authentication.
88
+ def wsse_password_node
89
+ return wsse_password unless digest?
90
+
91
+ token = wsse_nonce + wsse_timestamp + wsse_password
92
+ Base64.encode64(Digest::SHA1.hexdigest(token)).chomp!
93
+ end
94
+
95
+ # Returns a WSSE nonce.
96
+ def wsse_nonce
97
+ @wsse_nonce ||= Digest::SHA1.hexdigest random_string + wsse_timestamp
98
+ end
99
+
100
+ # Returns a WSSE timestamp.
101
+ def wsse_timestamp
102
+ @wsse_timestamp ||= Time.now.strftime Savon::SOAPDateTimeFormat
103
+ end
104
+
105
+ # Returns a random String of a given +length+.
106
+ def random_string(length = 100)
107
+ (0...length).map { ("a".."z").to_a[rand(26)] }.join
108
+ end
109
+
110
+ end
111
+ end