savon 1.2.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG.md +119 -104
- data/README.md +12 -11
- data/Rakefile +0 -6
- data/lib/savon.rb +16 -14
- data/lib/savon/block_interface.rb +26 -0
- data/lib/savon/builder.rb +142 -0
- data/lib/savon/client.rb +36 -135
- data/lib/savon/header.rb +42 -0
- data/lib/savon/http_error.rb +27 -0
- data/lib/savon/log_message.rb +23 -25
- data/lib/savon/message.rb +35 -0
- data/lib/savon/mock.rb +5 -0
- data/lib/savon/mock/expectation.rb +70 -0
- data/lib/savon/mock/spec_helper.rb +62 -0
- data/lib/savon/model.rb +39 -61
- data/lib/savon/operation.rb +62 -0
- data/lib/savon/options.rb +265 -0
- data/lib/savon/qualified_message.rb +49 -0
- data/lib/savon/request.rb +92 -0
- data/lib/savon/response.rb +97 -0
- data/lib/savon/soap_fault.rb +40 -0
- data/lib/savon/version.rb +1 -1
- data/savon.gemspec +10 -8
- data/spec/integration/options_spec.rb +536 -0
- data/spec/integration/request_spec.rb +31 -16
- data/spec/integration/support/application.rb +80 -0
- data/spec/integration/support/server.rb +84 -0
- data/spec/savon/builder_spec.rb +81 -0
- data/spec/savon/client_spec.rb +90 -488
- data/spec/savon/http_error_spec.rb +49 -0
- data/spec/savon/log_message_spec.rb +33 -0
- data/spec/savon/mock_spec.rb +127 -0
- data/spec/savon/model_spec.rb +110 -99
- data/spec/savon/observers_spec.rb +92 -0
- data/spec/savon/operation_spec.rb +49 -0
- data/spec/savon/request_spec.rb +145 -0
- data/spec/savon/{soap/response_spec.rb → response_spec.rb} +22 -59
- data/spec/savon/soap_fault_spec.rb +94 -0
- data/spec/spec_helper.rb +5 -3
- data/spec/support/fixture.rb +5 -1
- metadata +202 -197
- data/lib/savon/config.rb +0 -46
- data/lib/savon/error.rb +0 -6
- data/lib/savon/hooks/group.rb +0 -68
- data/lib/savon/hooks/hook.rb +0 -61
- data/lib/savon/http/error.rb +0 -42
- data/lib/savon/logger.rb +0 -39
- data/lib/savon/null_logger.rb +0 -10
- data/lib/savon/soap.rb +0 -21
- data/lib/savon/soap/fault.rb +0 -59
- data/lib/savon/soap/invalid_response_error.rb +0 -13
- data/lib/savon/soap/request.rb +0 -86
- data/lib/savon/soap/request_builder.rb +0 -205
- data/lib/savon/soap/response.rb +0 -117
- data/lib/savon/soap/xml.rb +0 -257
- data/spec/savon/config_spec.rb +0 -38
- data/spec/savon/hooks/group_spec.rb +0 -71
- data/spec/savon/hooks/hook_spec.rb +0 -16
- data/spec/savon/http/error_spec.rb +0 -52
- data/spec/savon/logger_spec.rb +0 -51
- data/spec/savon/savon_spec.rb +0 -33
- data/spec/savon/soap/fault_spec.rb +0 -89
- data/spec/savon/soap/request_builder_spec.rb +0 -207
- data/spec/savon/soap/request_spec.rb +0 -112
- data/spec/savon/soap/xml_spec.rb +0 -357
- 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/
|
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
|
-
|
10
|
-
|
9
|
+
Version 2
|
10
|
+
---------
|
11
11
|
|
12
|
-
Savon is
|
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
|
-
|
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.
|
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.
|
32
|
-
|
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
|
data/lib/savon.rb
CHANGED
@@ -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
|
-
|
10
|
-
|
11
|
-
end
|
3
|
+
class Error < RuntimeError; end
|
4
|
+
class InitializationError < Error; end
|
5
|
+
class InvalidResponseError < Error; end
|
12
6
|
|
13
|
-
def
|
14
|
-
|
7
|
+
def self.client(globals = {}, &block)
|
8
|
+
Client.new(globals, &block)
|
15
9
|
end
|
16
10
|
|
17
|
-
def
|
18
|
-
@
|
11
|
+
def self.observers
|
12
|
+
@observers ||= []
|
19
13
|
end
|
20
14
|
|
21
|
-
|
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
|
data/lib/savon/client.rb
CHANGED
@@ -1,162 +1,63 @@
|
|
1
|
-
require "
|
2
|
-
require "
|
3
|
-
require "
|
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
|
-
|
18
|
-
|
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
|
-
|
38
|
-
wsdl.request = http
|
39
|
-
end
|
12
|
+
BlockInterface.new(@globals).evaluate(block) if block
|
40
13
|
|
41
|
-
|
42
|
-
|
14
|
+
unless wsdl_or_endpoint_and_namespace_specified?
|
15
|
+
raise_initialization_error!
|
16
|
+
end
|
43
17
|
|
44
|
-
|
45
|
-
|
46
|
-
@wsdl
|
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
|
-
|
50
|
-
def http
|
51
|
-
@http ||= HTTPI::Request.new
|
52
|
-
end
|
24
|
+
attr_reader :globals
|
53
25
|
|
54
|
-
|
55
|
-
|
56
|
-
@
|
26
|
+
def operations
|
27
|
+
raise_missing_wsdl_error! unless @wsdl.document?
|
28
|
+
@wsdl.soap_actions
|
57
29
|
end
|
58
30
|
|
59
|
-
|
60
|
-
|
61
|
-
|
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
|
-
|
99
|
-
|
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
|
-
|
126
|
-
|
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
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
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
|
-
|
158
|
-
|
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
|