savon 0.3.2 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- data/README.textile +68 -0
- data/Rakefile +9 -7
- data/VERSION +1 -1
- data/lib/savon.rb +33 -34
- data/lib/savon/client.rb +96 -0
- data/lib/savon/core_ext.rb +6 -0
- data/lib/savon/core_ext/datetime.rb +8 -0
- data/lib/savon/core_ext/hash.rb +65 -0
- data/lib/savon/core_ext/object.rb +14 -0
- data/lib/savon/core_ext/string.rb +41 -0
- data/lib/savon/core_ext/symbol.rb +8 -0
- data/lib/savon/core_ext/uri.rb +10 -0
- data/lib/savon/request.rb +103 -0
- data/lib/savon/soap.rb +71 -0
- data/lib/savon/validation.rb +57 -0
- data/lib/savon/wsdl.rb +39 -41
- data/lib/savon/wsse.rb +111 -0
- data/spec/fixtures/multiple_user_response.xml +22 -0
- data/spec/fixtures/soap_fault.xml +0 -0
- data/spec/fixtures/user_fixture.rb +42 -0
- data/spec/fixtures/user_response.xml +4 -2
- data/spec/fixtures/user_wsdl.xml +0 -0
- data/spec/http_stubs.rb +20 -0
- data/spec/savon/client_spec.rb +144 -0
- data/spec/savon/core_ext/datetime_spec.rb +12 -0
- data/spec/savon/core_ext/hash_spec.rb +146 -0
- data/spec/savon/core_ext/object_spec.rb +26 -0
- data/spec/savon/core_ext/string_spec.rb +52 -0
- data/spec/savon/core_ext/symbol_spec.rb +11 -0
- data/spec/savon/core_ext/uri_spec.rb +15 -0
- data/spec/savon/request_spec.rb +93 -0
- data/spec/savon/savon_spec.rb +37 -0
- data/spec/savon/soap_spec.rb +101 -0
- data/spec/savon/validation_spec.rb +88 -0
- data/spec/savon/wsdl_spec.rb +17 -46
- data/spec/savon/wsse_spec.rb +169 -0
- data/spec/spec_helper.rb +7 -92
- data/spec/spec_helper_methods.rb +29 -0
- metadata +68 -20
- data/README.rdoc +0 -62
- data/lib/savon/service.rb +0 -151
- 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
|
data/lib/savon/soap.rb
ADDED
@@ -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
|
data/lib/savon/wsdl.rb
CHANGED
@@ -1,70 +1,68 @@
|
|
1
1
|
module Savon
|
2
2
|
|
3
|
-
# Savon::WSDL
|
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
|
-
#
|
7
|
-
def
|
8
|
-
@
|
10
|
+
# Expects a Savon::Request object.
|
11
|
+
def initialize(request)
|
12
|
+
@request = request
|
9
13
|
end
|
10
14
|
|
11
|
-
# Returns
|
12
|
-
def
|
13
|
-
@
|
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
|
17
|
-
def
|
18
|
-
|
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
|
-
#
|
22
|
-
def
|
23
|
-
@
|
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
|
32
|
+
# Returns the WSDL or +nil+ in case the WSDL could not be retrieved.
|
27
33
|
def to_s
|
28
|
-
|
34
|
+
wsdl_response ? wsdl_response.body : nil
|
29
35
|
end
|
30
36
|
|
31
37
|
private
|
32
38
|
|
33
|
-
#
|
34
|
-
#
|
35
|
-
def
|
36
|
-
unless @
|
37
|
-
@
|
38
|
-
@
|
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
|
-
@
|
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.
|
48
|
-
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
|
-
|
54
|
-
|
55
|
-
|
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
|
data/lib/savon/wsse.rb
ADDED
@@ -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
|