savon 1.2.0 → 2.0.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.
Files changed (66) hide show
  1. data/CHANGELOG.md +119 -104
  2. data/README.md +12 -11
  3. data/Rakefile +0 -6
  4. data/lib/savon.rb +16 -14
  5. data/lib/savon/block_interface.rb +26 -0
  6. data/lib/savon/builder.rb +142 -0
  7. data/lib/savon/client.rb +36 -135
  8. data/lib/savon/header.rb +42 -0
  9. data/lib/savon/http_error.rb +27 -0
  10. data/lib/savon/log_message.rb +23 -25
  11. data/lib/savon/message.rb +35 -0
  12. data/lib/savon/mock.rb +5 -0
  13. data/lib/savon/mock/expectation.rb +70 -0
  14. data/lib/savon/mock/spec_helper.rb +62 -0
  15. data/lib/savon/model.rb +39 -61
  16. data/lib/savon/operation.rb +62 -0
  17. data/lib/savon/options.rb +265 -0
  18. data/lib/savon/qualified_message.rb +49 -0
  19. data/lib/savon/request.rb +92 -0
  20. data/lib/savon/response.rb +97 -0
  21. data/lib/savon/soap_fault.rb +40 -0
  22. data/lib/savon/version.rb +1 -1
  23. data/savon.gemspec +10 -8
  24. data/spec/integration/options_spec.rb +536 -0
  25. data/spec/integration/request_spec.rb +31 -16
  26. data/spec/integration/support/application.rb +80 -0
  27. data/spec/integration/support/server.rb +84 -0
  28. data/spec/savon/builder_spec.rb +81 -0
  29. data/spec/savon/client_spec.rb +90 -488
  30. data/spec/savon/http_error_spec.rb +49 -0
  31. data/spec/savon/log_message_spec.rb +33 -0
  32. data/spec/savon/mock_spec.rb +127 -0
  33. data/spec/savon/model_spec.rb +110 -99
  34. data/spec/savon/observers_spec.rb +92 -0
  35. data/spec/savon/operation_spec.rb +49 -0
  36. data/spec/savon/request_spec.rb +145 -0
  37. data/spec/savon/{soap/response_spec.rb → response_spec.rb} +22 -59
  38. data/spec/savon/soap_fault_spec.rb +94 -0
  39. data/spec/spec_helper.rb +5 -3
  40. data/spec/support/fixture.rb +5 -1
  41. metadata +202 -197
  42. data/lib/savon/config.rb +0 -46
  43. data/lib/savon/error.rb +0 -6
  44. data/lib/savon/hooks/group.rb +0 -68
  45. data/lib/savon/hooks/hook.rb +0 -61
  46. data/lib/savon/http/error.rb +0 -42
  47. data/lib/savon/logger.rb +0 -39
  48. data/lib/savon/null_logger.rb +0 -10
  49. data/lib/savon/soap.rb +0 -21
  50. data/lib/savon/soap/fault.rb +0 -59
  51. data/lib/savon/soap/invalid_response_error.rb +0 -13
  52. data/lib/savon/soap/request.rb +0 -86
  53. data/lib/savon/soap/request_builder.rb +0 -205
  54. data/lib/savon/soap/response.rb +0 -117
  55. data/lib/savon/soap/xml.rb +0 -257
  56. data/spec/savon/config_spec.rb +0 -38
  57. data/spec/savon/hooks/group_spec.rb +0 -71
  58. data/spec/savon/hooks/hook_spec.rb +0 -16
  59. data/spec/savon/http/error_spec.rb +0 -52
  60. data/spec/savon/logger_spec.rb +0 -51
  61. data/spec/savon/savon_spec.rb +0 -33
  62. data/spec/savon/soap/fault_spec.rb +0 -89
  63. data/spec/savon/soap/request_builder_spec.rb +0 -207
  64. data/spec/savon/soap/request_spec.rb +0 -112
  65. data/spec/savon/soap/xml_spec.rb +0 -357
  66. data/spec/savon/soap_spec.rb +0 -16
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- Savon [![Build Status](https://secure.travis-ci.org/rubiii/savon.png?branch=master)](http://travis-ci.org/rubiii/savon)
1
+ Savon [![Build Status](https://secure.travis-ci.org/savonrb/savon.png?branch=version2)](http://travis-ci.org/savonrb/savon)
2
2
  =====
3
3
 
4
4
  Heavy metal SOAP client
@@ -6,13 +6,14 @@ Heavy metal SOAP client
6
6
  [Documentation](http://savonrb.com) | [RDoc](http://rubydoc.info/gems/savon) |
7
7
  [Mailing list](https://groups.google.com/forum/#!forum/savonrb) | [Twitter](http://twitter.com/savonrb)
8
8
 
9
- Installation
10
- ------------
9
+ Version 2
10
+ ---------
11
11
 
12
- Savon is available through [Rubygems](http://rubygems.org/gems/savon) and can be installed via:
12
+ Savon 2.0 is almost feature-complete and I would really appreciate your feedback!
13
+ To get started, add the following line to your Gemfile:
13
14
 
14
- ```
15
- $ gem install savon
15
+ ``` ruby
16
+ gem "savon", github: "savonrb/savon", branch: "version2"
16
17
  ```
17
18
 
18
19
  Introduction
@@ -22,14 +23,14 @@ Introduction
22
23
  require "savon"
23
24
 
24
25
  # create a client for your SOAP service
25
- client = Savon.client("http://service.example.com?wsdl")
26
+ client = Savon.client(wsdl: "http://service.example.com?wsdl")
26
27
 
27
- client.wsdl.soap_actions
28
+ client.operations
28
29
  # => [:create_user, :get_user, :get_all_users]
29
30
 
30
31
  # execute a SOAP request to call the "getUser" action
31
- response = client.request(:get_user) do
32
- soap.body = { :id => 1 }
32
+ response = client.call(:get_user) do
33
+ message(user_id: 1)
33
34
  end
34
35
 
35
36
  response.body
@@ -39,4 +40,4 @@ response.body
39
40
  Documentation
40
41
  -------------
41
42
 
42
- Continue reading at [savonrb.com](http://savonrb.com)
43
+ Continue reading at [savonrb.com](http://savonrb.com/version2.html)
data/Rakefile CHANGED
@@ -10,11 +10,5 @@ RSpec::Core::RakeTask.new "spec:integration" do |t|
10
10
  t.pattern = "spec/integration/**/*_spec.rb"
11
11
  end
12
12
 
13
- # http://stackoverflow.com/q/5771758/279024
14
- task :require_ruby_18 do
15
- raise "This must be run on Ruby 1.8" unless RUBY_VERSION =~ /^1\.8/
16
- end
17
- task :release => [:require_ruby_18]
18
-
19
13
  task :default => :spec
20
14
  task :test => :spec
@@ -1,23 +1,25 @@
1
- require "savon/version"
2
- require "savon/config"
3
- require "savon/client"
4
- require "savon/model"
5
-
6
1
  module Savon
7
- extend self
8
2
 
9
- def client(*args, &block)
10
- Client.new(*args, &block)
11
- end
3
+ class Error < RuntimeError; end
4
+ class InitializationError < Error; end
5
+ class InvalidResponseError < Error; end
12
6
 
13
- def configure
14
- yield config
7
+ def self.client(globals = {}, &block)
8
+ Client.new(globals, &block)
15
9
  end
16
10
 
17
- def config
18
- @config ||= Config.default
11
+ def self.observers
12
+ @observers ||= []
19
13
  end
20
14
 
21
- attr_writer :config
15
+ def self.notify_observers(operation_name, builder, globals, locals)
16
+ observers.inject(nil) do |response, observer|
17
+ observer.notify(operation_name, builder, globals, locals)
18
+ end
19
+ end
22
20
 
23
21
  end
22
+
23
+ require "savon/version"
24
+ require "savon/client"
25
+ require "savon/model"
@@ -0,0 +1,26 @@
1
+ module Savon
2
+ class BlockInterface
3
+
4
+ def initialize(target)
5
+ @target = target
6
+ end
7
+
8
+ def evaluate(block)
9
+ if block.arity > 0
10
+ block.call(@target)
11
+ else
12
+ @original = eval("self", block.binding)
13
+ instance_eval(&block)
14
+ end
15
+ end
16
+
17
+ private
18
+
19
+ def method_missing(method, *args, &block)
20
+ @target.send(method, *args, &block)
21
+ rescue NoMethodError
22
+ @original.send(method, *args, &block)
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,142 @@
1
+ require "savon/header"
2
+ require "savon/message"
3
+ require "builder"
4
+ require "gyoku"
5
+
6
+ module Savon
7
+ class Builder
8
+
9
+ SCHEMA_TYPES = {
10
+ "xmlns:xsd" => "http://www.w3.org/2001/XMLSchema",
11
+ "xmlns:xsi" => "http://www.w3.org/2001/XMLSchema-instance"
12
+ }
13
+
14
+ SOAP_NAMESPACE = {
15
+ 1 => "http://schemas.xmlsoap.org/soap/envelope/",
16
+ 2 => "http://www.w3.org/2003/05/soap-envelope"
17
+ }
18
+
19
+ def initialize(operation_name, wsdl, globals, locals)
20
+ @operation_name = operation_name
21
+
22
+ @wsdl = wsdl
23
+ @globals = globals
24
+ @locals = locals
25
+
26
+ @types = convert_type_definitions_to_hash
27
+ @used_namespaces = convert_type_namespaces_to_hash
28
+ end
29
+
30
+ def to_s
31
+ return @locals[:xml] if @locals.include? :xml
32
+
33
+ tag(builder, :Envelope, namespaces) do |xml|
34
+ tag(xml, :Header) { xml << header.to_s } unless header.empty?
35
+ tag(xml, :Body) { xml.tag!(*namespaced_message_tag) { xml << message.to_s } }
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def convert_type_definitions_to_hash
42
+ @wsdl.type_definitions.inject({}) do |memo, (path, type)|
43
+ memo[path] = type
44
+ memo
45
+ end
46
+ end
47
+
48
+ def convert_type_namespaces_to_hash
49
+ @wsdl.type_namespaces.inject({}) do |memo, (path, uri)|
50
+ key, value = use_namespace(path, uri)
51
+ memo[key] = value
52
+ memo
53
+ end
54
+ end
55
+
56
+ def use_namespace(path, uri)
57
+ @internal_namespace_count ||= 0
58
+
59
+ unless identifier = namespace_by_uri(uri)
60
+ identifier = "ins#{@internal_namespace_count}"
61
+ namespaces["xmlns:#{identifier}"] = uri
62
+ @internal_namespace_count += 1
63
+ end
64
+
65
+ [path, identifier]
66
+ end
67
+
68
+ def namespaces
69
+ @namespaces ||= begin
70
+ namespaces = SCHEMA_TYPES.dup
71
+ namespaces["xmlns:#{namespace_identifier}"] = @globals[:namespace] || @wsdl.namespace
72
+
73
+ key = ["xmlns"]
74
+ key << env_namespace if env_namespace && env_namespace != ""
75
+ namespaces[key.join(":")] = SOAP_NAMESPACE[@globals[:soap_version]]
76
+
77
+ namespaces
78
+ end
79
+ end
80
+
81
+ def env_namespace
82
+ @env_namespace ||= @globals[:env_namespace] || :env
83
+ end
84
+
85
+ def header
86
+ @header ||= Header.new(@globals, @locals)
87
+ end
88
+
89
+ def namespaced_message_tag
90
+ return [namespace_identifier, message_tag] unless @used_namespaces[[@operation_name.to_s]]
91
+ [@used_namespaces[[@operation_name.to_s]], message_tag]
92
+ end
93
+
94
+ def message_tag
95
+ message_tag = @locals[:message_tag]
96
+ message_tag ||= @wsdl.soap_input(@operation_name.to_sym) if @wsdl.document?
97
+ message_tag ||= Gyoku.xml_tag(@operation_name, :key_converter => @globals[:convert_request_keys_to])
98
+
99
+ @message_tag = message_tag.to_sym
100
+ end
101
+
102
+ def message
103
+ element_form_default = @globals[:element_form_default] || @wsdl.element_form_default
104
+ # TODO: clean this up! [dh, 2012-12-17]
105
+ Message.new(@operation_name, namespace_identifier, @types, @used_namespaces, @locals[:message],
106
+ element_form_default, @globals[:convert_request_keys_to])
107
+ end
108
+
109
+ def namespace_identifier
110
+ return @globals[:namespace_identifier] if @globals.include? :namespace_identifier
111
+ return @namespace_identifier if @namespace_identifier
112
+
113
+ operation = @wsdl.operations[@operation_name] if @wsdl.document?
114
+ namespace_identifier = operation[:namespace_identifier] if operation
115
+ namespace_identifier ||= "wsdl"
116
+
117
+ @namespace_identifier = namespace_identifier.to_sym
118
+ end
119
+
120
+ def namespace_by_uri(uri)
121
+ namespaces.each do |candidate_identifier, candidate_uri|
122
+ return candidate_identifier.gsub(/^xmlns:/, '') if candidate_uri == uri
123
+ end
124
+ nil
125
+ end
126
+
127
+ def builder
128
+ builder = ::Builder::XmlMarkup.new
129
+ builder.instruct!(:xml, :encoding => @globals[:encoding])
130
+ builder
131
+ end
132
+
133
+ def tag(xml, name, namespaces = {}, &block)
134
+ if env_namespace && env_namespace != ""
135
+ xml.tag! env_namespace, name, namespaces, &block
136
+ else
137
+ xml.tag! name, namespaces, &block
138
+ end
139
+ end
140
+
141
+ end
142
+ end
@@ -1,162 +1,63 @@
1
- require "wasabi/document"
2
- require "httpi/request"
3
- require "akami"
4
-
5
- require "savon/soap/xml"
6
- require "savon/soap/request"
7
- require "savon/soap/response"
8
- require "savon/soap/request_builder"
1
+ require "savon/operation"
2
+ require "savon/options"
3
+ require "savon/block_interface"
4
+ require "wasabi"
9
5
 
10
6
  module Savon
11
-
12
- # = Savon::Client
13
- #
14
- # Savon::Client is the main object for connecting to a SOAP service.
15
7
  class Client
16
8
 
17
- # Initializes the Savon::Client for a SOAP service. Accepts a +block+ which is evaluated in the
18
- # context of this object to let you access the +wsdl+, +http+, and +wsse+ methods.
19
- #
20
- # == Examples
21
- #
22
- # # Using a remote WSDL
23
- # client = Savon::Client.new("http://example.com/UserService?wsdl")
24
- #
25
- # # Using a local WSDL
26
- # client = Savon::Client.new File.expand_path("../wsdl/service.xml", __FILE__)
27
- #
28
- # # Directly accessing a SOAP endpoint
29
- # client = Savon::Client.new do
30
- # wsdl.endpoint = "http://example.com/UserService"
31
- # wsdl.namespace = "http://users.example.com"
32
- # end
33
- def initialize(wsdl_document = nil, &block)
34
- self.config = Savon.config.clone
35
- wsdl.document = wsdl_document if wsdl_document
9
+ def initialize(globals = {}, &block)
10
+ @globals = GlobalOptions.new(globals)
36
11
 
37
- process 1, &block if block
38
- wsdl.request = http
39
- end
12
+ BlockInterface.new(@globals).evaluate(block) if block
40
13
 
41
- # Accessor for the <tt>Savon::Config</tt>.
42
- attr_accessor :config
14
+ unless wsdl_or_endpoint_and_namespace_specified?
15
+ raise_initialization_error!
16
+ end
43
17
 
44
- # Returns the <tt>Wasabi::Document</tt>.
45
- def wsdl
46
- @wsdl ||= Wasabi::Document.new
18
+ @wsdl = Wasabi::Document.new
19
+ @wsdl.document = @globals[:wsdl] if @globals.include? :wsdl
20
+ @wsdl.endpoint = @globals[:endpoint] if @globals.include? :endpoint
21
+ @wsdl.namespace = @globals[:namespace] if @globals.include? :namespace
47
22
  end
48
23
 
49
- # Returns the <tt>HTTPI::Request</tt>.
50
- def http
51
- @http ||= HTTPI::Request.new
52
- end
24
+ attr_reader :globals
53
25
 
54
- # Returns the <tt>Akami::WSSE</tt> object.
55
- def wsse
56
- @wsse ||= Akami.wsse
26
+ def operations
27
+ raise_missing_wsdl_error! unless @wsdl.document?
28
+ @wsdl.soap_actions
57
29
  end
58
30
 
59
- # Executes a SOAP request for a given SOAP action. Accepts a +block+ which is evaluated in the
60
- # context of the <tt>SOAP::RequestBuilder</tt> object to let you access its +soap+, +wsdl+,
61
- # +http+ and +wsse+ methods.
62
- #
63
- # == Examples
64
- #
65
- # # Calls a "getUser" SOAP action with the payload of "<userId>123</userId>"
66
- # client.request(:get_user) { soap.body = { :user_id => 123 } }
67
- #
68
- # # Prefixes the SOAP input tag with a given namespace: "<wsdl:GetUser>...</wsdl:GetUser>"
69
- # client.request(:wsdl, "GetUser") { soap.body = { :user_id => 123 } }
70
- #
71
- # # SOAP input tag with attributes: <getUser xmlns:wsdl="http://example.com">...</getUser>"
72
- # client.request(:get_user, "xmlns:wsdl" => "http://example.com")
73
- def request(*args, &block)
74
- raise ArgumentError, "Savon::Client#request requires at least one argument" if args.empty?
75
-
76
- options = extract_options(args)
77
-
78
- request_builder = SOAP::RequestBuilder.new(options.delete(:input), options)
79
- request_builder.wsdl = wsdl
80
- request_builder.http = http.dup
81
- request_builder.wsse = wsse.dup
82
- request_builder.config = config.dup
83
-
84
- post_configuration = lambda { process(0, request_builder, &block) if block }
85
-
86
- response = request_builder.request(&post_configuration).response
87
- http.set_cookies(response.http)
88
-
89
- if wsse.verify_response
90
- WSSE::VerifySignature.new(response.http.body).verify!
91
- end
31
+ def operation(operation_name)
32
+ Operation.create(operation_name, @wsdl, @globals)
33
+ end
92
34
 
35
+ def call(operation_name, locals = {}, &block)
36
+ response = operation(operation_name).call(locals, &block)
37
+ persist_last_response(response)
93
38
  response
94
39
  end
95
40
 
96
41
  private
97
42
 
98
- # Expects an Array of +args+ and returns a Hash containing the SOAP input,
99
- # the namespace (might be +nil+), the SOAP action (might be +nil+),
100
- # the SOAP body (might be +nil+), and a Hash of attributes for the input
101
- # tag (which might be empty).
102
- def extract_options(args)
103
- attributes = Hash === args.last ? args.pop : {}
104
- body = attributes.delete(:body)
105
- soap_action = attributes.delete(:soap_action)
106
-
107
- namespace_identifier = args.size > 1 ? args.shift.to_sym : nil
108
- input = args.first
109
-
110
- remove_blank_values(
111
- :namespace_identifier => namespace_identifier,
112
- :input => input,
113
- :attributes => attributes,
114
- :body => body,
115
- :soap_action => soap_action
116
- )
117
- end
118
-
119
- # Processes a given +block+. Yields objects if the block expects any arguments.
120
- # Otherwise evaluates the block in the context of +instance+.
121
- def process(offset = 0, instance = self, &block)
122
- block.arity > 0 ? yield_objects(offset, instance, &block) : evaluate(instance, &block)
43
+ def persist_last_response(response)
44
+ @globals[:last_response] = response.http
123
45
  end
124
46
 
125
- # Yields a number of objects to a given +block+ depending on how many arguments
126
- # the block is expecting.
127
- def yield_objects(offset, instance, &block)
128
- to_yield = [:soap, :wsdl, :http, :wsse]
129
- yield *(to_yield[offset, block.arity].map { |obj_name| instance.send(obj_name) })
47
+ def wsdl_or_endpoint_and_namespace_specified?
48
+ @globals.include?(:wsdl) || (@globals.include?(:endpoint) && @globals.include?(:namespace))
130
49
  end
131
50
 
132
- # Evaluates a given +block+ inside +instance+. Stores the original block binding.
133
- def evaluate(instance, &block)
134
- original_self = eval "self", block.binding
135
-
136
- # A proxy that attemps to make method calls on +instance+. If a NoMethodError is
137
- # raised, the call will be made on +original_self+.
138
- proxy = Object.new
139
- proxy.instance_eval do
140
- class << self
141
- attr_accessor :original_self, :instance
142
- end
143
-
144
- def method_missing(method, *args, &block)
145
- instance.send(method, *args, &block)
146
- rescue NoMethodError
147
- original_self.send(method, *args, &block)
148
- end
149
- end
150
-
151
- proxy.instance = instance
152
- proxy.original_self = original_self
153
-
154
- proxy.instance_eval &block
51
+ def raise_initialization_error!
52
+ raise InitializationError,
53
+ "Expected either a WSDL document or the SOAP endpoint and target namespace options.\n\n" \
54
+ "Savon.client(wsdl: '/Users/me/project/service.wsdl') # to use a local WSDL document\n" \
55
+ "Savon.client(wsdl: 'http://example.com?wsdl') # to use a remote WSDL document\n" \
56
+ "Savon.client(endpoint: 'http://example.com', namespace: 'http://v1.example.com') # if you don't have a WSDL document"
155
57
  end
156
58
 
157
- # Removes all blank values from a given +hash+.
158
- def remove_blank_values(hash)
159
- hash.delete_if { |_, value| value.respond_to?(:empty?) ? value.empty? : !value }
59
+ def raise_missing_wsdl_error!
60
+ raise "Unable to inspect the service without a WSDL document."
160
61
  end
161
62
 
162
63
  end