savon 0.3.2 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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