s-savon 0.8.6

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 (61) hide show
  1. data/.gitignore +9 -0
  2. data/.rspec +1 -0
  3. data/.yardopts +2 -0
  4. data/CHANGELOG.md +461 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +20 -0
  7. data/README.md +37 -0
  8. data/Rakefile +40 -0
  9. data/lib/savon.rb +14 -0
  10. data/lib/savon/client.rb +157 -0
  11. data/lib/savon/core_ext/hash.rb +70 -0
  12. data/lib/savon/core_ext/object.rb +14 -0
  13. data/lib/savon/core_ext/string.rb +51 -0
  14. data/lib/savon/core_ext/time.rb +14 -0
  15. data/lib/savon/error.rb +6 -0
  16. data/lib/savon/global.rb +75 -0
  17. data/lib/savon/http/error.rb +42 -0
  18. data/lib/savon/soap.rb +24 -0
  19. data/lib/savon/soap/fault.rb +59 -0
  20. data/lib/savon/soap/request.rb +61 -0
  21. data/lib/savon/soap/response.rb +80 -0
  22. data/lib/savon/soap/xml.rb +187 -0
  23. data/lib/savon/version.rb +5 -0
  24. data/lib/savon/wsdl/document.rb +112 -0
  25. data/lib/savon/wsdl/parser.rb +102 -0
  26. data/lib/savon/wsdl/request.rb +35 -0
  27. data/lib/savon/wsse.rb +150 -0
  28. data/savon.gemspec +29 -0
  29. data/spec/fixtures/gzip/message.gz +0 -0
  30. data/spec/fixtures/response/another_soap_fault.xml +14 -0
  31. data/spec/fixtures/response/authentication.xml +14 -0
  32. data/spec/fixtures/response/header.xml +13 -0
  33. data/spec/fixtures/response/list.xml +18 -0
  34. data/spec/fixtures/response/multi_ref.xml +39 -0
  35. data/spec/fixtures/response/soap_fault.xml +8 -0
  36. data/spec/fixtures/response/soap_fault12.xml +18 -0
  37. data/spec/fixtures/wsdl/authentication.xml +63 -0
  38. data/spec/fixtures/wsdl/geotrust.xml +156 -0
  39. data/spec/fixtures/wsdl/namespaced_actions.xml +307 -0
  40. data/spec/fixtures/wsdl/no_namespace.xml +115 -0
  41. data/spec/fixtures/wsdl/two_bindings.xml +25 -0
  42. data/spec/savon/client_spec.rb +346 -0
  43. data/spec/savon/core_ext/hash_spec.rb +121 -0
  44. data/spec/savon/core_ext/object_spec.rb +19 -0
  45. data/spec/savon/core_ext/string_spec.rb +57 -0
  46. data/spec/savon/core_ext/time_spec.rb +13 -0
  47. data/spec/savon/http/error_spec.rb +52 -0
  48. data/spec/savon/savon_spec.rb +85 -0
  49. data/spec/savon/soap/fault_spec.rb +89 -0
  50. data/spec/savon/soap/request_spec.rb +45 -0
  51. data/spec/savon/soap/response_spec.rb +174 -0
  52. data/spec/savon/soap/xml_spec.rb +335 -0
  53. data/spec/savon/soap_spec.rb +21 -0
  54. data/spec/savon/wsdl/document_spec.rb +132 -0
  55. data/spec/savon/wsdl/parser_spec.rb +99 -0
  56. data/spec/savon/wsdl/request_spec.rb +15 -0
  57. data/spec/savon/wsse_spec.rb +213 -0
  58. data/spec/spec_helper.rb +14 -0
  59. data/spec/support/endpoint.rb +25 -0
  60. data/spec/support/fixture.rb +37 -0
  61. metadata +251 -0
@@ -0,0 +1,5 @@
1
+ module Savon
2
+
3
+ Version = "0.8.6"
4
+
5
+ end
@@ -0,0 +1,112 @@
1
+ require "rexml/document"
2
+
3
+ require "savon/wsdl/request"
4
+ require "savon/wsdl/parser"
5
+
6
+ module Savon
7
+ module WSDL
8
+
9
+ # = Savon::WSDL::Document
10
+ #
11
+ # Represents the WSDL of your service, including information like the namespace URI,
12
+ # the SOAP endpoint and available SOAP actions.
13
+ class Document
14
+
15
+ # Accepts an <tt>HTTPI::Request</tt> and a +document+.
16
+ def initialize(request = nil, document = nil)
17
+ self.request = request
18
+ self.document = document
19
+ end
20
+
21
+ # Accessor for the <tt>HTTPI::Request</tt> to use.
22
+ attr_accessor :request
23
+
24
+ def present?
25
+ !!@document
26
+ end
27
+
28
+ # Returns the namespace URI of the WSDL.
29
+ def namespace
30
+ @namespace ||= parser.namespace
31
+ end
32
+
33
+ # Sets the SOAP namespace.
34
+ attr_writer :namespace
35
+
36
+ # Returns the SOAP endpoint.
37
+ def endpoint
38
+ @endpoint ||= parser.endpoint
39
+ end
40
+
41
+ # Sets the SOAP endpoint.
42
+ attr_writer :endpoint
43
+
44
+ # Returns an Array of available SOAP actions.
45
+ def soap_actions
46
+ @soap_actions ||= parser.operations.keys
47
+ end
48
+
49
+ # Returns the SOAP action for a given +key+.
50
+ def soap_action(key)
51
+ operations[key][:action] if present? && operations[key]
52
+ end
53
+
54
+ # Returns the SOAP input for a given +key+.
55
+ def soap_input(key)
56
+ operations[key][:input].to_sym if present? && operations[key]
57
+ end
58
+
59
+ # Returns a Hash of SOAP operations.
60
+ def operations
61
+ @operations ||= parser.operations
62
+ end
63
+
64
+ # Returns the elementFormDefault value.
65
+ def element_form_default
66
+ @element_form_default ||= parser.element_form_default
67
+ end
68
+
69
+ # Sets the location of the WSDL document to use. This can either be a URL
70
+ # or a path to a local file.
71
+ attr_writer :document
72
+
73
+ # Returns the raw WSDL document.
74
+ def document
75
+ @wsdl_document ||= begin
76
+ raise ArgumentError, "No WSDL document given" if @document.blank?
77
+ remote? ? http_request : read_file
78
+ end
79
+ end
80
+
81
+ alias :to_xml :document
82
+
83
+ private
84
+
85
+ # Returns whether the WSDL document is located on the Web.
86
+ def remote?
87
+ @document =~ /^http/
88
+ end
89
+
90
+ # Executes an HTTP GET request to retrieve a remote WSDL document.
91
+ def http_request
92
+ request.url = @document
93
+ Request.new(request).response.body
94
+ end
95
+
96
+ # Reads the WSDL document from a local file.
97
+ def read_file
98
+ File.read @document
99
+ end
100
+
101
+ # Parses the WSDL document and returns the <tt>Savon::WSDL::Parser</tt>.
102
+ def parser
103
+ @parser ||= begin
104
+ parser = Parser.new
105
+ REXML::Document.parse_stream document, parser
106
+ parser
107
+ end
108
+ end
109
+
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,102 @@
1
+ require "savon/core_ext/object"
2
+ require "savon/core_ext/string"
3
+
4
+ module Savon
5
+ module WSDL
6
+
7
+ # = Savon::WSDL::Parser
8
+ #
9
+ # Serves as a stream listener for parsing WSDL documents.
10
+ class Parser
11
+
12
+ # The main sections of a WSDL document.
13
+ Sections = %w(definitions types message portType binding service)
14
+
15
+ def initialize
16
+ @path = []
17
+ @operations = {}
18
+ @namespaces = {}
19
+ @element_form_default = :unqualified
20
+ end
21
+
22
+ # Returns the namespace URI.
23
+ attr_reader :namespace
24
+
25
+ # Returns the SOAP operations.
26
+ attr_reader :operations
27
+
28
+ # Returns the SOAP endpoint.
29
+ attr_reader :endpoint
30
+
31
+ # Returns the elementFormDefault value.
32
+ attr_reader :element_form_default
33
+
34
+ # Hook method called when the stream parser encounters a starting tag.
35
+ def tag_start(tag, attrs)
36
+ # read xml namespaces if root element
37
+ read_namespaces(attrs) if @path.empty?
38
+
39
+ tag, namespace = tag.split(":").reverse
40
+ @path << tag
41
+
42
+ if @section == :types && tag == "schema"
43
+ @element_form_default = attrs["elementFormDefault"].to_sym if attrs["elementFormDefault"]
44
+ end
45
+
46
+ if @section == :binding && tag == "binding"
47
+ # ensure that we are in an wsdl/soap namespace
48
+ @section = nil unless @namespaces[namespace].starts_with? "http://schemas.xmlsoap.org/wsdl/soap"
49
+ end
50
+
51
+ @section = tag.to_sym if Sections.include?(tag) && depth <= 2
52
+
53
+ @namespace ||= attrs["targetNamespace"] if @section == :definitions
54
+ @endpoint ||= URI(URI.escape(attrs["location"])) if @section == :service && tag == "address"
55
+
56
+ operation_from tag, attrs if @section == :binding && tag == "operation"
57
+ end
58
+
59
+ # Returns our current depth in the WSDL document.
60
+ def depth
61
+ @path.size
62
+ end
63
+
64
+ # Reads namespace definitions from a given +attrs+ Hash.
65
+ def read_namespaces(attrs)
66
+ attrs.each do |key, value|
67
+ @namespaces[key.strip_namespace] = value if key.starts_with? "xmlns:"
68
+ end
69
+ end
70
+
71
+ # Hook method called when the stream parser encounters a closing tag.
72
+ def tag_end(tag)
73
+ @path.pop
74
+
75
+ if @section == :binding && @input && tag.strip_namespace == "operation"
76
+ # no soapAction attribute found till now
77
+ operation_from tag, "soapAction" => @input
78
+ end
79
+
80
+ @section = :definitions if Sections.include?(tag) && depth <= 1
81
+ end
82
+
83
+ # Stores available operations from a given tag +name+ and +attrs+.
84
+ def operation_from(tag, attrs)
85
+ @input = attrs["name"] if attrs["name"]
86
+
87
+ if attrs["soapAction"]
88
+ @action = !attrs["soapAction"].blank? ? attrs["soapAction"] : @input
89
+ @input = @action.split("/").last if !@input || @input.empty?
90
+
91
+ @operations[@input.snakecase.to_sym] = { :action => @action, :input => @input }
92
+ @input, @action = nil, nil
93
+ end
94
+ end
95
+
96
+ # Catches calls to unimplemented hook methods.
97
+ def method_missing(method, *args)
98
+ end
99
+
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,35 @@
1
+ require "httpi"
2
+
3
+ module Savon
4
+ module WSDL
5
+
6
+ # = Savon::WSDL::Request
7
+ #
8
+ # Executes WSDL requests.
9
+ class Request
10
+
11
+ # Expects an <tt>HTTPI::Request</tt>.
12
+ def initialize(request)
13
+ self.request = request
14
+ end
15
+
16
+ # Accessor for the <tt>HTTPI::Request</tt>.
17
+ attr_accessor :request
18
+
19
+ # Executes the request and returns the response.
20
+ def response
21
+ @response ||= with_logging { HTTPI.get request }
22
+ end
23
+
24
+ private
25
+
26
+ # Logs the HTTP request and yields to a given +block+.
27
+ def with_logging
28
+ Savon.log "Retrieving WSDL from: #{request.url}"
29
+ Savon.log "Using :#{request.auth.type} authentication" if request.auth?
30
+ yield
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,150 @@
1
+ require "base64"
2
+ require "digest/sha1"
3
+
4
+ require "savon/core_ext/string"
5
+ require "savon/core_ext/hash"
6
+ require "savon/core_ext/time"
7
+
8
+ module Savon
9
+
10
+ # = Savon::WSSE
11
+ #
12
+ # Provides WSSE authentication.
13
+ class WSSE
14
+
15
+ # Namespace for WS Security Secext.
16
+ WSENamespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd"
17
+
18
+ # Namespace for WS Security Utility.
19
+ WSUNamespace = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd"
20
+
21
+ # URI for "wsse:Password/@Type" #PasswordText.
22
+ PasswordTextURI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordText"
23
+
24
+ # URI for "wsse:Password/@Type" #PasswordDigest.
25
+ PasswordDigestURI = "http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest"
26
+
27
+ # Returns a value from the WSSE Hash.
28
+ def [](key)
29
+ hash[key]
30
+ end
31
+
32
+ # Sets a value on the WSSE Hash.
33
+ def []=(key, value)
34
+ hash[key] = value
35
+ end
36
+
37
+ # Sets authentication credentials for a wsse:UsernameToken header.
38
+ # Also accepts whether to use WSSE digest authentication.
39
+ def credentials(username, password, digest = false)
40
+ self.username = username
41
+ self.password = password
42
+ self.digest = digest
43
+ end
44
+
45
+ attr_accessor :username, :password, :created_at, :expires_at
46
+
47
+ # Returns whether to use WSSE digest. Defaults to +false+.
48
+ def digest?
49
+ !!@digest
50
+ end
51
+
52
+ attr_writer :digest
53
+
54
+ # Returns whether to generate a wsse:UsernameToken header.
55
+ def username_token?
56
+ username && password
57
+ end
58
+
59
+ # Returns whether to generate a wsse:Timestamp header.
60
+ def timestamp?
61
+ created_at || expires_at || @wsse_timestamp
62
+ end
63
+
64
+ # Sets whether to generate a wsse:Timestamp header.
65
+ def timestamp=(timestamp)
66
+ @wsse_timestamp = timestamp
67
+ end
68
+
69
+ # Returns the XML for a WSSE header.
70
+ def to_xml
71
+ if username_token?
72
+ Gyoku.xml wsse_username_token.merge!(hash)
73
+ elsif timestamp?
74
+ Gyoku.xml wsse_timestamp.merge!(hash)
75
+ else
76
+ ""
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ # Returns a Hash containing wsse:UsernameToken details.
83
+ def wsse_username_token
84
+ if digest?
85
+ wsse_security "UsernameToken",
86
+ "wsse:Username" => username,
87
+ "wsse:Nonce" => nonce,
88
+ "wsu:Created" => timestamp,
89
+ "wsse:Password" => digest_password,
90
+ :attributes! => { "wsse:Password" => { "Type" => PasswordDigestURI } }
91
+ else
92
+ wsse_security "UsernameToken",
93
+ "wsse:Username" => username,
94
+ "wsse:Password" => password,
95
+ :attributes! => { "wsse:Password" => { "Type" => PasswordTextURI } }
96
+ end
97
+ end
98
+
99
+ # Returns a Hash containing wsse:Timestamp details.
100
+ def wsse_timestamp
101
+ wsse_security "Timestamp",
102
+ "wsu:Created" => (created_at || Time.now).xs_datetime,
103
+ "wsu:Expires" => (expires_at || (created_at || Time.now) + 60).xs_datetime
104
+ end
105
+
106
+ # Returns a Hash containing wsse:Security details for a given +tag+ and +hash+.
107
+ def wsse_security(tag, hash)
108
+ {
109
+ "wsse:Security" => {
110
+ "wsse:#{tag}" => hash,
111
+ :attributes! => { "wsse:#{tag}" => { "wsu:Id" => "#{tag}-#{count}", "xmlns:wsu" => WSUNamespace } }
112
+ },
113
+ :attributes! => { "wsse:Security" => { "xmlns:wsse" => WSENamespace } }
114
+ }
115
+ end
116
+
117
+ # Returns the WSSE password, encrypted for digest authentication.
118
+ def digest_password
119
+ token = nonce + timestamp + password
120
+ Base64.encode64(Digest::SHA1.hexdigest(token)).chomp!
121
+ end
122
+
123
+ # Returns a WSSE nonce.
124
+ def nonce
125
+ @nonce ||= Digest::SHA1.hexdigest random_string + timestamp
126
+ end
127
+
128
+ # Returns a random String of 100 characters.
129
+ def random_string
130
+ (0...100).map { ("a".."z").to_a[rand(26)] }.join
131
+ end
132
+
133
+ # Returns a WSSE timestamp.
134
+ def timestamp
135
+ @timestamp ||= Time.now.xs_datetime
136
+ end
137
+
138
+ # Returns a new number with every call.
139
+ def count
140
+ @count ||= 0
141
+ @count += 1
142
+ end
143
+
144
+ # Returns a memoized and autovivificating Hash.
145
+ def hash
146
+ @hash ||= Hash.new { |h, k| h[k] = Hash.new(&h.default_proc) }
147
+ end
148
+
149
+ end
150
+ end
@@ -0,0 +1,29 @@
1
+ lib = File.expand_path("../lib", __FILE__)
2
+ $:.unshift lib unless $:.include? lib
3
+
4
+ require "savon/version"
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "s-savon"
8
+ s.version = Savon::Version
9
+ s.authors = "Daniel Harrington"
10
+ s.email = "me@rubiii.com"
11
+ s.homepage = "http://github.com/rubiii/#{s.name}"
12
+ s.summary = "Heavy metal Ruby SOAP client"
13
+ s.description = "Savon is the heavy metal Ruby SOAP client."
14
+
15
+ s.rubyforge_project = s.name
16
+
17
+ s.add_dependency "builder", ">= 2.1.2"
18
+ s.add_dependency "crack", "~> 0.1.8"
19
+ s.add_dependency "httpi", ">= 0.7.8"
20
+ s.add_dependency "gyoku", ">= 0.3.0"
21
+
22
+ s.add_development_dependency "rspec", "~> 2.4.0"
23
+ s.add_development_dependency "autotest"
24
+ s.add_development_dependency "mocha", "~> 0.9.8"
25
+ s.add_development_dependency "timecop", "~> 0.3.5"
26
+
27
+ s.files = `git ls-files`.split("\n")
28
+ s.require_path = "lib"
29
+ end
@@ -0,0 +1,14 @@
1
+ <SOAP-ENV:Envelope SOAP-ENV:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/"
2
+ xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"
3
+ xmlns:SOAP-ENC="http://schemas.xmlsoap.org/soap/encoding/"
4
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
5
+ xmlns:xsd="http://www.w3.org/2001/XMLSchema">
6
+ <SOAP-ENV:Body>
7
+ <SOAP-ENV:Fault>
8
+ <faultcode>ERR_NO_SESSION</faultcode>
9
+ <faultfactor>doGetItemsInfo - Wrong session</faultfactor>
10
+ <faultstring>Wrong session message</faultstring>
11
+ <detail><soapVal><ERRNO xsi:type="xsd:string">80:1289245853:55</ERRNO></soapVal></detail>
12
+ </SOAP-ENV:Fault>
13
+ </SOAP-ENV:Body>
14
+ </SOAP-ENV:Envelope>