savon 1.2.0 → 2.0.0

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