tictoc-savon 0.7.9

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 (59) hide show
  1. data/.autotest +5 -0
  2. data/CHANGELOG +176 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +64 -0
  5. data/Rakefile +50 -0
  6. data/lib/savon.rb +35 -0
  7. data/lib/savon/client.rb +131 -0
  8. data/lib/savon/core_ext.rb +8 -0
  9. data/lib/savon/core_ext/array.rb +31 -0
  10. data/lib/savon/core_ext/datetime.rb +10 -0
  11. data/lib/savon/core_ext/hash.rb +107 -0
  12. data/lib/savon/core_ext/net_http.rb +19 -0
  13. data/lib/savon/core_ext/object.rb +16 -0
  14. data/lib/savon/core_ext/string.rb +69 -0
  15. data/lib/savon/core_ext/symbol.rb +8 -0
  16. data/lib/savon/core_ext/uri.rb +10 -0
  17. data/lib/savon/logger.rb +56 -0
  18. data/lib/savon/request.rb +138 -0
  19. data/lib/savon/response.rb +174 -0
  20. data/lib/savon/soap.rb +302 -0
  21. data/lib/savon/version.rb +5 -0
  22. data/lib/savon/wsdl.rb +137 -0
  23. data/lib/savon/wsdl_stream.rb +85 -0
  24. data/lib/savon/wsse.rb +163 -0
  25. data/spec/basic_spec_helper.rb +11 -0
  26. data/spec/endpoint_helper.rb +23 -0
  27. data/spec/fixtures/gzip/gzip_response_fixture.rb +7 -0
  28. data/spec/fixtures/gzip/message.gz +0 -0
  29. data/spec/fixtures/response/response_fixture.rb +36 -0
  30. data/spec/fixtures/response/xml/authentication.xml +14 -0
  31. data/spec/fixtures/response/xml/multi_ref.xml +39 -0
  32. data/spec/fixtures/response/xml/soap_fault.xml +8 -0
  33. data/spec/fixtures/response/xml/soap_fault12.xml +18 -0
  34. data/spec/fixtures/wsdl/wsdl_fixture.rb +37 -0
  35. data/spec/fixtures/wsdl/wsdl_fixture.yml +42 -0
  36. data/spec/fixtures/wsdl/xml/authentication.xml +63 -0
  37. data/spec/fixtures/wsdl/xml/geotrust.xml +156 -0
  38. data/spec/fixtures/wsdl/xml/namespaced_actions.xml +307 -0
  39. data/spec/fixtures/wsdl/xml/no_namespace.xml +115 -0
  40. data/spec/http_stubs.rb +26 -0
  41. data/spec/integration/http_basic_auth_spec.rb +16 -0
  42. data/spec/integration/server.rb +51 -0
  43. data/spec/savon/client_spec.rb +86 -0
  44. data/spec/savon/core_ext/array_spec.rb +49 -0
  45. data/spec/savon/core_ext/datetime_spec.rb +21 -0
  46. data/spec/savon/core_ext/hash_spec.rb +190 -0
  47. data/spec/savon/core_ext/net_http_spec.rb +38 -0
  48. data/spec/savon/core_ext/object_spec.rb +34 -0
  49. data/spec/savon/core_ext/string_spec.rb +99 -0
  50. data/spec/savon/core_ext/symbol_spec.rb +12 -0
  51. data/spec/savon/core_ext/uri_spec.rb +19 -0
  52. data/spec/savon/request_spec.rb +117 -0
  53. data/spec/savon/response_spec.rb +179 -0
  54. data/spec/savon/soap_spec.rb +202 -0
  55. data/spec/savon/wsdl_spec.rb +107 -0
  56. data/spec/savon/wsse_spec.rb +132 -0
  57. data/spec/spec.opts +4 -0
  58. data/spec/spec_helper.rb +5 -0
  59. metadata +229 -0
@@ -0,0 +1,8 @@
1
+ require "savon/core_ext/object"
2
+ require "savon/core_ext/string"
3
+ require "savon/core_ext/symbol"
4
+ require "savon/core_ext/datetime"
5
+ require "savon/core_ext/array"
6
+ require "savon/core_ext/hash"
7
+ require "savon/core_ext/uri"
8
+ require "savon/core_ext/net_http"
@@ -0,0 +1,31 @@
1
+ class Array
2
+
3
+ # Translates the Array into SOAP compatible XML. See: Hash.to_soap_xml.
4
+ def to_soap_xml(key, escape_xml = true, attributes = {})
5
+ xml = Builder::XmlMarkup.new
6
+
7
+ each_with_index do |item, index|
8
+ attrs = tag_attributes attributes, index
9
+ case item
10
+ when Hash then xml.tag!(key, attrs) { xml << item.to_soap_xml }
11
+ else xml.tag!(key, attrs) { xml << (escape_xml ? item.to_soap_value : item.to_soap_value!) }
12
+ end
13
+ end
14
+
15
+ xml.target!
16
+ end
17
+
18
+ private
19
+
20
+ # Takes a Hash of +attributes+ and the +index+ for which to return attributes
21
+ # for duplicate tags.
22
+ def tag_attributes(attributes, index)
23
+ return {} if attributes.empty?
24
+
25
+ attributes.inject({}) do |hash, (key, value)|
26
+ value = value[index] if value.kind_of? Array
27
+ hash.merge key => value
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,10 @@
1
+ class DateTime
2
+
3
+ # Returns the DateTime as an xs:dateTime formatted String.
4
+ def to_soap_value
5
+ strftime Savon::SOAP::DateTimeFormat
6
+ end
7
+
8
+ alias_method :to_soap_value!, :to_soap_value
9
+
10
+ end
@@ -0,0 +1,107 @@
1
+ class Hash
2
+
3
+ # Returns the values from the soap:Body element or an empty Hash in case the soap:Body tag could
4
+ # not be found.
5
+ def find_soap_body
6
+ envelope = self[keys.first] || {}
7
+ body_key = envelope.keys.find { |key| /.+:Body/ =~ key } rescue nil
8
+ body_key ? envelope[body_key].map_soap_response : {}
9
+ end
10
+
11
+ # Translates the Hash into SOAP request compatible XML.
12
+ #
13
+ # { :find_user => { :id => 123, "wsdl:Key" => "api" } }.to_soap_xml
14
+ # # => "<findUser><id>123</id><wsdl:Key>api</wsdl:Key></findUser>"
15
+ #
16
+ # ==== Mapping
17
+ #
18
+ # * Hash keys specified as Symbols are converted to lowerCamelCase Strings
19
+ # * Hash keys specified as Strings are not converted and may contain namespaces
20
+ # * DateTime values are converted to xs:dateTime Strings
21
+ # * Objects responding to to_datetime (except Strings) are converted to xs:dateTime Strings
22
+ # * TrueClass and FalseClass objects are converted to "true" and "false" Strings
23
+ # * All other objects are expected to be converted to Strings using to_s
24
+ #
25
+ # An example:
26
+ #
27
+ # { :magic_request => {
28
+ # :perform_move => true,
29
+ # "perform_at" => DateTime.new(2010, 11, 22, 11, 22, 33)
30
+ # }
31
+ # }.to_soap_xml
32
+ #
33
+ # <magicRequest>
34
+ # <performMove>true</performMove>
35
+ # <perform_at>2012-06-11T10:42:21</perform_at>
36
+ # </magicRequest>
37
+ #
38
+ # ==== Escaped XML values
39
+ #
40
+ # By default, special characters in XML String values are escaped.
41
+ #
42
+ # ==== Fixed order of XML tags
43
+ #
44
+ # In case your service requires the tags to be in a specific order (parameterOrder), you have two
45
+ # options. The first is to specify your body as an XML string. The second is to specify the order
46
+ # through an additional array stored under the +:order!+ key.
47
+ #
48
+ # { :name => "Eve", :id => 123, :order! => [:id, :name] }.to_soap_xml
49
+ # # => "<id>123</id><name>Eve</name>"
50
+ #
51
+ # ==== XML attributes
52
+ #
53
+ # If you need attributes, you could either go with an XML string or add another hash under the
54
+ # +:attributes!+ key.
55
+ #
56
+ # { :person => "Eve", :attributes! => { :person => { :id => 666 } } }.to_soap_xml
57
+ # # => '<person id="666">Eve</person>'
58
+ def to_soap_xml
59
+ xml = Builder::XmlMarkup.new
60
+ attributes = delete(:attributes!) || {}
61
+
62
+ order.each do |key|
63
+ attrs = attributes[key] || {}
64
+ value = self[key]
65
+ escape_xml = key.to_s[-1, 1] != "!"
66
+ key = key.to_soap_key
67
+
68
+ case value
69
+ when Array then xml << value.to_soap_xml(key, escape_xml, attrs)
70
+ when Hash then xml.tag!(key, attrs) { xml << value.to_soap_xml }
71
+ else xml.tag!(key, attrs) { xml << (escape_xml ? value.to_soap_value : value.to_soap_value!) }
72
+ end
73
+ end
74
+
75
+ xml.target!
76
+ end
77
+
78
+ # Maps keys and values of a Hash created from SOAP response XML to more convenient Ruby Objects.
79
+ def map_soap_response
80
+ inject({}) do |hash, (key, value)|
81
+ value = case value
82
+ when Hash then value["xsi:nil"] ? nil : value.map_soap_response
83
+ when Array then value.map { |val| val.map_soap_response rescue val }
84
+ when String then value.map_soap_response
85
+ end
86
+
87
+ hash.merge key.strip_namespace.snakecase.to_sym => value
88
+ end
89
+ end
90
+
91
+ private
92
+
93
+ # Deletes and returns an Array of keys stored under the :order! key. Defaults to return the actual
94
+ # keys of this Hash if no :order! key could be found. Raises an ArgumentError in case the :order!
95
+ # Array does not match the Hash keys.
96
+ def order
97
+ order = delete :order!
98
+ order = keys unless order.kind_of? Array
99
+
100
+ missing, spurious = keys - order, order - keys
101
+ raise ArgumentError, "Missing elements in :order! #{missing.inspect}" unless missing.empty?
102
+ raise ArgumentError, "Spurious elements in :order! #{spurious.inspect}" unless spurious.empty?
103
+
104
+ order
105
+ end
106
+
107
+ end
@@ -0,0 +1,19 @@
1
+ module Net
2
+ class HTTP
3
+
4
+ # Sets the endpoint +address+ and +port+.
5
+ def endpoint(address, port)
6
+ @address, @port = address, port
7
+ end
8
+
9
+ # Convenience method for setting SSL client authentication through a Hash of +options+.
10
+ def ssl_client_auth(options)
11
+ self.use_ssl = true
12
+ self.cert = options[:cert] if options[:cert]
13
+ self.key = options[:key] if options[:key]
14
+ self.ca_file = options[:ca_file] if options[:ca_file]
15
+ self.verify_mode = options[:verify_mode] if options[:verify_mode]
16
+ end
17
+
18
+ end
19
+ end
@@ -0,0 +1,16 @@
1
+ class Object
2
+
3
+ # Returns +true+ if the Object is nil, false or empty. Implementation from ActiveSupport.
4
+ def blank?
5
+ respond_to?(:empty?) ? empty? : !self
6
+ end unless defined? blank?
7
+
8
+ # Returns the Object as a SOAP request compliant value.
9
+ def to_soap_value
10
+ return to_s unless respond_to? :to_datetime
11
+ to_datetime.to_soap_value
12
+ end
13
+
14
+ alias_method :to_soap_value!, :to_soap_value
15
+
16
+ end
@@ -0,0 +1,69 @@
1
+ class String
2
+
3
+ # Returns a random String of a given +length+.
4
+ def self.random(length = 100)
5
+ (0...length).map { ("a".."z").to_a[rand(26)] }.join
6
+ end
7
+
8
+ # Returns the String in snake_case.
9
+ def snakecase
10
+ str = dup
11
+ str.gsub! /::/, '/'
12
+ str.gsub! /([A-Z]+)([A-Z][a-z])/, '\1_\2'
13
+ str.gsub! /([a-z\d])([A-Z])/, '\1_\2'
14
+ str.tr! ".", "_"
15
+ str.tr! "-", "_"
16
+ str.downcase!
17
+ str
18
+ end
19
+
20
+ # Returns the String in lowerCamelCase.
21
+ def lower_camelcase
22
+ str = dup
23
+ str.gsub!(/\/(.?)/) { "::#{$1.upcase}" }
24
+ str.gsub!(/(?:_+|-+)([a-z])/) { $1.upcase }
25
+ str.gsub!(/(\A|\s)([A-Z])/) { $1 + $2.downcase }
26
+ str
27
+ end
28
+
29
+ # Returns whether the String starts with a given +prefix+.
30
+ def starts_with?(prefix)
31
+ prefix = prefix.to_s
32
+ self[0, prefix.length] == prefix
33
+ end unless defined? starts_with?
34
+
35
+ # Returns whether the String ends with a given +suffix+.
36
+ def ends_with?(suffix)
37
+ suffix = suffix.to_s
38
+ self[-suffix.length, suffix.length] == suffix
39
+ end unless defined? ends_with?
40
+
41
+ # Returns the String without namespace.
42
+ def strip_namespace
43
+ split(":").last
44
+ end
45
+
46
+ # Translates SOAP response values to Ruby Objects.
47
+ def map_soap_response
48
+ return DateTime.parse(self) if Savon::SOAP::DateTimeRegexp === self
49
+ return true if self.strip.downcase == "true"
50
+ return false if self.strip.downcase == "false"
51
+ self
52
+ end
53
+
54
+ # Returns the Object as a SOAP request compliant key.
55
+ def to_soap_key
56
+ self[-1, 1] == "!" ? chop : self
57
+ end
58
+
59
+ # Returns the String as a SOAP value. Escapes special characters for XML.
60
+ def to_soap_value
61
+ CGI.escapeHTML self
62
+ end
63
+
64
+ # Convert the String into a SOAP value without escaping special characters.
65
+ def to_soap_value!
66
+ self
67
+ end
68
+
69
+ end
@@ -0,0 +1,8 @@
1
+ class Symbol
2
+
3
+ # Returns the Symbol as a lowerCamelCase String.
4
+ def to_soap_key
5
+ to_s.to_soap_key.lower_camelcase
6
+ end
7
+
8
+ end
@@ -0,0 +1,10 @@
1
+ module URI
2
+ class Generic
3
+
4
+ # Returns whether the URI hints to SSL.
5
+ def ssl?
6
+ !@scheme ? nil : @scheme.starts_with?("https")
7
+ end
8
+
9
+ end
10
+ end
@@ -0,0 +1,56 @@
1
+ module Savon
2
+
3
+ # = Savon::Logger
4
+ #
5
+ # Savon::Logger can be mixed into classes to provide logging behavior.
6
+ #
7
+ # By default, the Logger mixin uses {Ruby's Logger}[http://ruby-doc.org/stdlib/libdoc/logger/rdoc/]
8
+ # from the standard library, a log level of :debug and is pointing to STDOUT.
9
+ module Logger
10
+
11
+ module ClassMethods
12
+
13
+ # Sets whether to log.
14
+ def log=(log)
15
+ @log = log
16
+ end
17
+
18
+ # Returns whether to log. Defaults to +true+.
19
+ def log?
20
+ @log != false
21
+ end
22
+
23
+ # Sets the logger.
24
+ def logger=(logger)
25
+ @logger = logger
26
+ end
27
+
28
+ # Returns the logger. Defaults to an instance of +Logger+ writing to STDOUT.
29
+ def logger
30
+ @logger ||= ActiveRecord::Base.logger rescue ::Logger.new STDOUT
31
+ end
32
+
33
+ # Sets the log level.
34
+ def log_level=(log_level)
35
+ @log_level = log_level
36
+ end
37
+
38
+ # Returns the log level. Defaults to +debug+.
39
+ def log_level
40
+ @log_level ||= :debug
41
+ end
42
+
43
+ end
44
+
45
+ # Extends the class including this module with its ClassMethods.
46
+ def self.included(base)
47
+ base.extend ClassMethods
48
+ end
49
+
50
+ # Logs a given +message+.
51
+ def log(message)
52
+ self.class.logger.send self.class.log_level, message if self.class.log?
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,138 @@
1
+ module Savon
2
+
3
+ # = Savon::Request
4
+ #
5
+ # Savon::Request handles both WSDL and SOAP requests.
6
+ #
7
+ # == The Net::HTTP object
8
+ #
9
+ # You can access the Net::HTTP object used for both WSDL and SOAP requests via:
10
+ #
11
+ # client.request.http
12
+ #
13
+ # Here's an example of how to set open and read timeouts on the Net::HTTP object.
14
+ #
15
+ # client.request.http.open_timeout = 30
16
+ # client.request.http.read_timeout = 30
17
+ #
18
+ # Please refer to the {Net::HTTP documentation}[http://ruby-doc.org/stdlib/libdoc/net/http/rdoc/]
19
+ # for more information.
20
+ #
21
+ # == HTTP basic authentication
22
+ #
23
+ # Setting credentials for HTTP basic authentication:
24
+ #
25
+ # client.request.basic_auth "username", "password"
26
+ #
27
+ # == SSL client authentication
28
+ #
29
+ # You can use the methods provided by Net::HTTP to set SSL client authentication or use a shortcut:
30
+ #
31
+ # client.request.http.ssl_client_auth(
32
+ # :cert => OpenSSL::X509::Certificate.new(File.read("client_cert.pem")),
33
+ # :key => OpenSSL::PKey::RSA.new(File.read("client_key.pem"), "password if one exists"),
34
+ # :ca_file => "cacert.pem",
35
+ # :verify_mode => OpenSSL::SSL::VERIFY_PEER
36
+ # )
37
+ #
38
+ # == HTTP headers
39
+ #
40
+ # There's an accessor for the Hash of HTTP headers sent with any SOAP call:
41
+ #
42
+ # client.request.headers["custom"] = "header"
43
+ class Request
44
+ include Logger
45
+
46
+ # Content-Types by SOAP version.
47
+ ContentType = { 1 => "text/xml;charset=UTF-8", 2 => "application/soap+xml;charset=UTF-8" }
48
+
49
+ # Expects a WSDL or SOAP +endpoint+ and accepts a custom +proxy+ address.
50
+ def initialize(endpoint, options = {})
51
+ @endpoint = URI endpoint
52
+ @proxy = URI options[:proxy] || ""
53
+ headers["Accept-encoding"] = "gzip,deflate" if options[:gzip]
54
+ end
55
+
56
+ # Returns the endpoint URI.
57
+ attr_reader :endpoint
58
+
59
+ # Returns the proxy URI.
60
+ attr_reader :proxy
61
+
62
+ # Returns the HTTP headers for a SOAP request.
63
+ def headers
64
+ @headers ||= {}
65
+ end
66
+
67
+ # Sets the HTTP headers for a SOAP request.
68
+ def headers=(headers)
69
+ @headers = headers if headers.kind_of? Hash
70
+ end
71
+
72
+ # Sets the +username+ and +password+ for HTTP basic authentication.
73
+ def basic_auth(username, password)
74
+ @basic_auth = [username, password]
75
+ end
76
+
77
+ # Retrieves WSDL document and returns the Net::HTTP response.
78
+ def wsdl
79
+ log "Retrieving WSDL from: #{@endpoint}"
80
+ http.endpoint @endpoint.host, @endpoint.port
81
+ http.use_ssl = @endpoint.ssl?
82
+ http.start { |h| h.request request(:wsdl) }
83
+ end
84
+
85
+ # Executes a SOAP request using a given Savon::SOAP instance and returns the Net::HTTP response.
86
+ def soap(soap)
87
+ @soap = soap
88
+ http.endpoint @soap.endpoint.host, @soap.endpoint.port
89
+ http.use_ssl = @soap.endpoint.ssl?
90
+
91
+ log_request
92
+ @response = http.start do |h|
93
+ h.request request(:soap) { |request| request.body = @soap.to_xml }
94
+ end
95
+ log_response
96
+ @response
97
+ end
98
+
99
+ # Returns the Net::HTTP object.
100
+ def http
101
+ @http ||= Net::HTTP::Proxy(@proxy.host, @proxy.port).new @endpoint.host, @endpoint.port
102
+ end
103
+
104
+ private
105
+
106
+ # Logs the SOAP request.
107
+ def log_request
108
+ log "SOAP request: #{@soap.endpoint}"
109
+ log soap_headers.merge(headers).map { |key, value| "#{key}: #{value}" }.join(", ")
110
+ log @soap.to_xml
111
+ end
112
+
113
+ # Logs the SOAP response.
114
+ def log_response
115
+ log "SOAP response (status #{@response.code}):"
116
+ log @response.body
117
+ end
118
+
119
+ # Returns a Net::HTTP request for a given +type+. Yields the request to an optional block.
120
+ def request(type)
121
+ request = case type
122
+ when :wsdl then Net::HTTP::Get.new @endpoint.request_uri
123
+ when :soap then Net::HTTP::Post.new @soap.endpoint.request_uri, soap_headers.merge(headers)
124
+ end
125
+
126
+ request.basic_auth(*@basic_auth) if @basic_auth
127
+ yield request if block_given?
128
+ request
129
+ end
130
+
131
+ # Returns a Hash containing the SOAP headers for an HTTP request.
132
+ def soap_headers
133
+ { "Content-Type" => ContentType[@soap.version], "SOAPAction" => @soap.action }
134
+ end
135
+
136
+ end
137
+ end
138
+