julianmorrison-savon 0.6.8

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