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.
- 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
|