julianmorrison-savon 0.6.8

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/CHANGELOG +92 -0
  2. data/README.textile +71 -0
  3. data/Rakefile +27 -0
  4. data/lib/savon.rb +34 -0
  5. data/lib/savon/client.rb +84 -0
  6. data/lib/savon/core_ext.rb +3 -0
  7. data/lib/savon/core_ext/datetime.rb +8 -0
  8. data/lib/savon/core_ext/hash.rb +78 -0
  9. data/lib/savon/core_ext/object.rb +21 -0
  10. data/lib/savon/core_ext/string.rb +47 -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 +159 -0
  14. data/lib/savon/response.rb +108 -0
  15. data/lib/savon/soap.rb +138 -0
  16. data/lib/savon/wsdl.rb +122 -0
  17. data/lib/savon/wsse.rb +122 -0
  18. data/spec/endpoint_helper.rb +22 -0
  19. data/spec/fixtures/response/response_fixture.rb +32 -0
  20. data/spec/fixtures/response/xml/authentication.xml +14 -0
  21. data/spec/fixtures/response/xml/soap_fault.xml +8 -0
  22. data/spec/fixtures/response/xml/soap_fault12.xml +18 -0
  23. data/spec/fixtures/wsdl/wsdl_fixture.rb +37 -0
  24. data/spec/fixtures/wsdl/xml/authentication.xml +63 -0
  25. data/spec/fixtures/wsdl/xml/namespaced_actions.xml +307 -0
  26. data/spec/fixtures/wsdl/xml/no_namespace.xml +115 -0
  27. data/spec/http_stubs.rb +23 -0
  28. data/spec/savon/client_spec.rb +83 -0
  29. data/spec/savon/core_ext/datetime_spec.rb +12 -0
  30. data/spec/savon/core_ext/hash_spec.rb +134 -0
  31. data/spec/savon/core_ext/object_spec.rb +40 -0
  32. data/spec/savon/core_ext/string_spec.rb +68 -0
  33. data/spec/savon/core_ext/symbol_spec.rb +11 -0
  34. data/spec/savon/core_ext/uri_spec.rb +15 -0
  35. data/spec/savon/request_spec.rb +124 -0
  36. data/spec/savon/response_spec.rb +122 -0
  37. data/spec/savon/savon_spec.rb +23 -0
  38. data/spec/savon/soap_spec.rb +131 -0
  39. data/spec/savon/wsdl_spec.rb +84 -0
  40. data/spec/savon/wsse_spec.rb +132 -0
  41. data/spec/spec_helper.rb +16 -0
  42. metadata +166 -0
@@ -0,0 +1,8 @@
1
+ class Symbol
2
+
3
+ # Returns the Symbol as a lowerCamelCase String.
4
+ def to_soap_key
5
+ to_s.lower_camelcase
6
+ end
7
+
8
+ end
@@ -0,0 +1,10 @@
1
+ module URI
2
+ class HTTP
3
+
4
+ # Returns whether the URI hints to SSL.
5
+ def ssl?
6
+ /^https/ === @scheme
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,159 @@
1
+ module Savon
2
+
3
+ # == Savon::Request
4
+ #
5
+ # Handles both WSDL and SOAP HTTP requests.
6
+ class Request
7
+
8
+ # Content-Types by SOAP version.
9
+ ContentType = { 1 => "text/xml", 2 => "application/soap+xml" }
10
+
11
+ # Whether to log HTTP requests.
12
+ @@log = true
13
+
14
+ # The default logger.
15
+ @@logger = Logger.new STDOUT
16
+
17
+ # The default log level.
18
+ @@log_level = :debug
19
+
20
+ # Sets whether to log HTTP requests.
21
+ def self.log=(log)
22
+ @@log = log
23
+ end
24
+
25
+ # Returns whether to log HTTP requests.
26
+ def self.log?
27
+ @@log
28
+ end
29
+
30
+ # Sets the logger.
31
+ def self.logger=(logger)
32
+ @@logger = logger
33
+ end
34
+
35
+ # Returns the logger.
36
+ def self.logger
37
+ @@logger
38
+ end
39
+
40
+ # Sets the log level.
41
+ def self.log_level=(log_level)
42
+ @@log_level = log_level
43
+ end
44
+
45
+ # Returns the log level.
46
+ def self.log_level
47
+ @@log_level
48
+ end
49
+
50
+ # Expects a SOAP +endpoint+ String. Also accepts an optional Hash of
51
+ # +options+ for specifying a proxy server and SSL client authentication.
52
+ def initialize(endpoint, options = {})
53
+ @endpoint = URI endpoint
54
+ @proxy = options[:proxy] ? URI(options[:proxy]) : URI("")
55
+ @ssl = options[:ssl] if options[:ssl]
56
+ end
57
+
58
+ # Returns the endpoint URI.
59
+ attr_reader :endpoint
60
+
61
+ # Returns the proxy URI.
62
+ attr_reader :proxy
63
+
64
+ # Accessor for HTTP open timeout.
65
+ attr_accessor :open_timeout
66
+
67
+ # Accessor for HTTP read timeout.
68
+ attr_accessor :read_timeout
69
+
70
+ # Retrieves WSDL document and returns the Net::HTTPResponse.
71
+ def wsdl
72
+ log "Retrieving WSDL from: #{@endpoint}"
73
+
74
+ query = @endpoint.path
75
+ query += ('?' + @endpoint.query) if @endpoint.query
76
+ req = Net::HTTP::Get.new query
77
+ req.basic_auth(@endpoint.user, @endpoint.password) if @endpoint.user
78
+
79
+ http.start {|h| h.request(req) }
80
+ end
81
+
82
+ # Executes a SOAP request using a given Savon::SOAP instance and
83
+ # returns the Net::HTTPResponse.
84
+ def soap(soap)
85
+ @soap = soap
86
+
87
+ log_request
88
+
89
+ req = Net::HTTP::Post.new @soap.endpoint.path, http_header
90
+ req.body = @soap.to_xml
91
+ req.basic_auth(@soap.endpoint.user, @soap.endpoint.password) if @soap.endpoint.user
92
+
93
+ @response = http(@soap.endpoint).start {|h| h.request(req) }
94
+
95
+ log_response
96
+ @response
97
+ end
98
+
99
+ private
100
+
101
+ # Logs the SOAP request.
102
+ def log_request
103
+ log "SOAP request: #{@soap.endpoint}"
104
+ log http_header.map { |key, value| "#{key}: #{value}" }.join( ", " )
105
+ log @soap.to_xml
106
+ end
107
+
108
+ # Logs the SOAP response.
109
+ def log_response
110
+ log "SOAP response (status #{@response.code}):"
111
+ log @response.body
112
+ end
113
+
114
+ # Returns a Net::HTTP instance for a given +endpoint+.
115
+ def http(endpoint = @endpoint)
116
+ @http = Net::HTTP::Proxy(@proxy.host, @proxy.port).new endpoint.host, endpoint.port
117
+ set_http_timeout
118
+ set_ssl_options endpoint.ssl?
119
+ set_ssl_authentication if @ssl
120
+ @http
121
+ end
122
+
123
+ # Sets HTTP open and read timeout.
124
+ def set_http_timeout
125
+ @http.open_timeout = @open_timeout if @open_timeout
126
+ @http.read_timeout = @read_timeout if @read_timeout
127
+ end
128
+
129
+ # Sets basic SSL options to the +@http+ instance.
130
+ def set_ssl_options(use_ssl)
131
+ @http.use_ssl = use_ssl
132
+ @http.verify_mode = OpenSSL::SSL::VERIFY_NONE
133
+ end
134
+
135
+ # Sets SSL client authentication to the +@http+ instance.
136
+ def set_ssl_authentication
137
+ @http.verify_mode = @ssl[:verify] if @ssl[:verify].kind_of? Integer
138
+ @http.cert = @ssl[:client_cert] if @ssl[:client_cert]
139
+ @http.key = @ssl[:client_key] if @ssl[:client_key]
140
+ @http.ca_file = @ssl[:ca_file] if @ssl[:ca_file]
141
+ end
142
+
143
+ # Returns a Hash containing the header for an HTTP request.
144
+ def http_header
145
+ { "Content-Type" => ContentType[@soap.version], "SOAPAction" => @soap.action }
146
+ end
147
+
148
+ # Logs a given +message+.
149
+ def log(message)
150
+ self.class.logger.send self.class.log_level, message if log?
151
+ end
152
+
153
+ # Returns whether logging is possible.
154
+ def log?
155
+ self.class.log? && self.class.logger.respond_to?(self.class.log_level)
156
+ end
157
+
158
+ end
159
+ end
@@ -0,0 +1,108 @@
1
+ module Savon
2
+
3
+ # == Savon::Response
4
+ #
5
+ # Represents the HTTP and SOAP response.
6
+ class Response
7
+
8
+ # The global setting of whether to raise errors.
9
+ @@raise_errors = true
10
+
11
+ # Sets the global setting of whether to raise errors.
12
+ def self.raise_errors=(raise_errors)
13
+ @@raise_errors = raise_errors
14
+ end
15
+
16
+ # Returns the global setting of whether to raise errors.
17
+ def self.raise_errors?
18
+ @@raise_errors
19
+ end
20
+
21
+ # Expects a Net::HTTPResponse and handles errors.
22
+ def initialize(response)
23
+ @response = response
24
+
25
+ handle_soap_fault
26
+ handle_http_error
27
+ end
28
+
29
+ # Returns whether there was a SOAP fault.
30
+ def soap_fault?
31
+ @soap_fault
32
+ end
33
+
34
+ # Returns the SOAP fault message.
35
+ attr_reader :soap_fault
36
+
37
+ # Returns whether there was an HTTP error.
38
+ def http_error?
39
+ @http_error
40
+ end
41
+
42
+ # Returns the HTTP error message.
43
+ attr_reader :http_error
44
+
45
+ # Returns the SOAP response as a Hash.
46
+ def to_hash
47
+ @body.find_regexp(/.+/).map_soap_response
48
+ end
49
+
50
+ # Returns the SOAP response XML.
51
+ def to_xml
52
+ @response.body
53
+ end
54
+
55
+ alias :to_s :to_xml
56
+
57
+ private
58
+
59
+ # Returns the SOAP response body as a Hash.
60
+ def body
61
+ unless @body
62
+ body = Crack::XML.parse @response.body
63
+ @body = body.find_regexp [/.+:Envelope/, /.+:Body/]
64
+ end
65
+ @body
66
+ end
67
+
68
+ # Handles SOAP faults. Raises a Savon::SOAPFault unless the default
69
+ # behavior of raising errors was turned off.
70
+ def handle_soap_fault
71
+ if soap_fault_message
72
+ @soap_fault = soap_fault_message
73
+ raise Savon::SOAPFault, soap_fault_message if self.class.raise_errors?
74
+ end
75
+ end
76
+
77
+ # Returns a SOAP fault message in case a SOAP fault was found.
78
+ def soap_fault_message
79
+ unless @soap_fault_message
80
+ soap_fault = body.find_regexp [/.+:Fault/]
81
+ @soap_fault_message = soap_fault_message_by_version(soap_fault)
82
+ end
83
+ @soap_fault_message
84
+ end
85
+
86
+ # Expects a Hash that might contain information about a SOAP fault.
87
+ # Returns the SOAP fault message in case one was found.
88
+ def soap_fault_message_by_version(soap_fault)
89
+ if soap_fault.keys.include? "faultcode"
90
+ "(#{soap_fault['faultcode']}) #{soap_fault['faultstring']}"
91
+ elsif soap_fault.keys.include? "Code"
92
+ # SOAP 1.2 error code element is capitalized, see: http://www.w3.org/TR/soap12-part1/#faultcodeelement
93
+ "(#{soap_fault['Code']['Value']}) #{soap_fault['Reason']['Text']}"
94
+ end
95
+ end
96
+
97
+ # Handles HTTP errors. Raises a Savon::HTTPError unless the default
98
+ # behavior of raising errors was turned off.
99
+ def handle_http_error
100
+ if @response.code.to_i >= 300
101
+ @http_error = "#{@response.message} (#{@response.code})"
102
+ @http_error << ": #{@response.body}" unless @response.body.empty?
103
+ raise Savon::HTTPError, http_error if self.class.raise_errors?
104
+ end
105
+ end
106
+
107
+ end
108
+ end
@@ -0,0 +1,138 @@
1
+ module Savon
2
+
3
+ # == Savon::SOAP
4
+ #
5
+ # Represents the SOAP parameters and envelope.
6
+ class SOAP
7
+
8
+ # SOAP namespaces by SOAP version.
9
+ SOAPNamespace = {
10
+ 1 => "http://schemas.xmlsoap.org/soap/envelope/",
11
+ 2 => "http://www.w3.org/2003/05/soap-envelope"
12
+ }
13
+
14
+ # Content-Types by SOAP version.
15
+ ContentType = { 1 => "text/xml", 2 => "application/soap+xml" }
16
+
17
+ # The global SOAP version.
18
+ @@version = 1
19
+
20
+ # Returns the global SOAP version.
21
+ def self.version
22
+ @@version
23
+ end
24
+
25
+ # Sets the global SOAP version.
26
+ def self.version=(version)
27
+ @@version = version if Savon::SOAPVersions.include? version
28
+ end
29
+
30
+ def initialize
31
+ @builder = Builder::XmlMarkup.new
32
+ end
33
+
34
+ # Sets the WSSE options.
35
+ attr_writer :wsse
36
+
37
+ # Sets the SOAP action.
38
+ attr_writer :action
39
+
40
+ # Returns the SOAP action.
41
+ def action
42
+ @action ||= ""
43
+ end
44
+
45
+ # Sets the SOAP input.
46
+ attr_writer :input
47
+
48
+ # Returns the SOAP input.
49
+ def input
50
+ @input ||= ""
51
+ end
52
+
53
+ # Accessor for the SOAP endpoint.
54
+ attr_accessor :endpoint
55
+
56
+ # Sets the SOAP header. Expected to be a Hash that can be translated
57
+ # to XML via Hash.to_soap_xml or any other Object responding to to_s.
58
+ attr_writer :header
59
+
60
+ # Returns the SOAP header. Defaults to an empty Hash.
61
+ def header
62
+ @header ||= {}
63
+ end
64
+
65
+ # Sets the SOAP body. Expected to be a Hash that can be translated to
66
+ # XML via Hash.to_soap_xml or any other Object responding to to_s.
67
+ attr_writer :body
68
+
69
+ # Sets the namespaces. Expected to be a Hash containing the namespaces
70
+ # (keys) and the corresponding URI's (values).
71
+ attr_writer :namespaces
72
+
73
+ # Returns the namespaces. A Hash containing the namespaces (keys) and
74
+ # the corresponding URI's (values).
75
+ def namespaces
76
+ @namespaces ||= { "xmlns:env" => SOAPNamespace[version] }
77
+ end
78
+
79
+ # Convenience method for setting the "xmlns:wsdl" namespace.
80
+ def namespace=(namespace)
81
+ namespaces["xmlns:wsdl"] = namespace
82
+ end
83
+
84
+ # Sets the SOAP version.
85
+ def version=(version)
86
+ @version = version if Savon::SOAPVersions.include? version
87
+ end
88
+
89
+ # Returns the SOAP version. Defaults to the global default.
90
+ def version
91
+ @version ||= self.class.version
92
+ end
93
+
94
+ # Returns the SOAP envelope XML.
95
+ def to_xml
96
+ unless @xml_body
97
+ @xml_body = @builder.env :Envelope, namespaces do |xml|
98
+ xml_header xml
99
+ xml_body xml
100
+ end
101
+ end
102
+ @xml_body
103
+ end
104
+
105
+ private
106
+
107
+ # Adds a SOAP XML header to a given +xml+ Object.
108
+ def xml_header(xml)
109
+ xml.env(:Header) do
110
+ xml << (header.to_soap_xml rescue header.to_s) + wsse_header
111
+ end
112
+ end
113
+
114
+ # Adds a SOAP XML body to a given +xml+ Object.
115
+ def xml_body(xml)
116
+ xml.env(:Body) do
117
+ xml.tag!(:wsdl, *input_array) do
118
+ xml << (@body.to_soap_xml rescue @body.to_s)
119
+ end
120
+ end
121
+ end
122
+
123
+ # Returns an Array of SOAP input names to append to the :wsdl namespace.
124
+ # Defaults to use the name of the SOAP action and may be an empty Array
125
+ # in case the specified SOAP input seems invalid.
126
+ def input_array
127
+ return [input.to_sym] unless input.blank?
128
+ return [action.to_sym] unless action.blank?
129
+ []
130
+ end
131
+
132
+ # Returns the WSSE header or an empty String in case WSSE was not set.
133
+ def wsse_header
134
+ @wsse.respond_to?(:header) ? @wsse.header : ""
135
+ end
136
+
137
+ end
138
+ end
@@ -0,0 +1,122 @@
1
+ module Savon
2
+
3
+ # Savon::WSDL
4
+ #
5
+ # Represents the WSDL document.
6
+ class WSDL
7
+
8
+ # Initializer, expects a Savon::Request.
9
+ def initialize(request)
10
+ @request = request
11
+ end
12
+
13
+ # Sets whether to use the WSDL.
14
+ attr_writer :enabled
15
+
16
+ # Returns whether to use the WSDL. Defaults to +true+.
17
+ def enabled?
18
+ @enabled.nil? ? true : @enabled
19
+ end
20
+
21
+ # Returns the namespace URI of the WSDL.
22
+ def namespace_uri
23
+ @namespace_uri ||= stream.namespace_uri
24
+ end
25
+
26
+ # Returns an Array of available SOAP actions.
27
+ def soap_actions
28
+ @soap_actions ||= stream.operations.keys
29
+ end
30
+
31
+ # Returns a Hash of SOAP operations including their corresponding
32
+ # SOAP actions and inputs.
33
+ def operations
34
+ @operations ||= stream.operations
35
+ end
36
+
37
+ # Returns the SOAP endpoint.
38
+ def soap_endpoint
39
+ @soap_endpoint ||= stream.soap_endpoint
40
+ end
41
+
42
+ # Returns +true+ for available methods and SOAP actions.
43
+ def respond_to?(method)
44
+ return true if soap_actions.include? method
45
+ super
46
+ end
47
+
48
+ # Returns the raw WSDL document.
49
+ def to_s
50
+ @document ||= @request.wsdl.body
51
+ end
52
+
53
+ private
54
+
55
+ # Returns the Savon::WSDLStream.
56
+ def stream
57
+ unless @stream
58
+ @stream = WSDLStream.new
59
+ REXML::Document.parse_stream to_s, @stream
60
+ end
61
+ @stream
62
+ end
63
+
64
+ end
65
+
66
+ # Savon::WSDLStream
67
+ #
68
+ # Stream listener for parsing the WSDL document.
69
+ class WSDLStream
70
+
71
+ # The main sections of a WSDL document.
72
+ Sections = %w(definitions types message portType binding service)
73
+
74
+ def initialize
75
+ @depth, @operations = 0, {}
76
+ end
77
+
78
+ # Returns the namespace URI.
79
+ attr_reader :namespace_uri
80
+
81
+ # Returns the SOAP operations.
82
+ attr_reader :operations
83
+
84
+ # Returns the SOAP endpoint.
85
+ attr_reader :soap_endpoint
86
+
87
+ # Hook method called when the stream parser encounters a starting tag.
88
+ def tag_start(tag, attrs)
89
+ @depth += 1
90
+ tag = tag.strip_namespace
91
+
92
+ @section = tag.to_sym if @depth <= 2 && Sections.include?(tag)
93
+ @namespace_uri ||= attrs["targetNamespace"] if @section == :definitions
94
+ @soap_endpoint ||= URI(attrs["location"]) if @section == :service && tag == "address"
95
+
96
+ operation_from tag, attrs if @section == :binding && tag == "operation"
97
+ end
98
+
99
+ # Hook method called when the stream parser encounters a closing tag.
100
+ def tag_end(tag)
101
+ @depth -= 1
102
+ end
103
+
104
+ # Stores available operations from a given tag +name+ and +attrs+.
105
+ def operation_from(tag, attrs)
106
+ @input = attrs["name"] if attrs["name"]
107
+
108
+ if attrs["soapAction"]
109
+ @action = !attrs["soapAction"].blank? ? attrs["soapAction"] : @input
110
+ @input = @action.split("/").last if !@input || @input.empty?
111
+
112
+ @operations[@input.snakecase.to_sym] = { :action => @action, :input => @input }
113
+ @input, @action = nil, nil
114
+ end
115
+ end
116
+
117
+ # Catches calls to unimplemented hook methods.
118
+ def method_missing(method, *args)
119
+ end
120
+
121
+ end
122
+ end