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/lib/savon/header.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require "akami"
|
2
|
+
require "gyoku"
|
3
|
+
|
4
|
+
module Savon
|
5
|
+
class Header
|
6
|
+
|
7
|
+
def initialize(globals, locals)
|
8
|
+
@globals = globals
|
9
|
+
@locals = locals
|
10
|
+
@wsse = create_wsse
|
11
|
+
end
|
12
|
+
|
13
|
+
def empty?
|
14
|
+
to_s.empty?
|
15
|
+
end
|
16
|
+
|
17
|
+
def to_s
|
18
|
+
return @header if @header
|
19
|
+
|
20
|
+
gyoku_options = { :key_converter => @globals[:convert_request_keys_to] }
|
21
|
+
@header = (Hash === header ? Gyoku.xml(header, gyoku_options) : header) + wsse_header
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def create_wsse
|
27
|
+
wsse = Akami.wsse
|
28
|
+
wsse.credentials(*@globals[:wsse_auth]) if @globals.include? :wsse_auth
|
29
|
+
wsse.timestamp = @globals[:wsse_timestamp] if @globals.include? :wsse_timestamp
|
30
|
+
wsse
|
31
|
+
end
|
32
|
+
|
33
|
+
def header
|
34
|
+
@header ||= @globals.include?(:soap_header) ? @globals[:soap_header] : {}
|
35
|
+
end
|
36
|
+
|
37
|
+
def wsse_header
|
38
|
+
@wsse.respond_to?(:to_xml) ? @wsse.to_xml : ""
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require "savon"
|
2
|
+
|
3
|
+
module Savon
|
4
|
+
class HTTPError < Error
|
5
|
+
|
6
|
+
def self.present?(http)
|
7
|
+
http.error?
|
8
|
+
end
|
9
|
+
|
10
|
+
def initialize(http)
|
11
|
+
@http = http
|
12
|
+
end
|
13
|
+
|
14
|
+
attr_reader :http
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
message = "HTTP error (#{@http.code})"
|
18
|
+
message << ": #{@http.body}" unless @http.body.empty?
|
19
|
+
message
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_hash
|
23
|
+
{ :code => @http.code, :headers => @http.headers, :body => @http.body }
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
data/lib/savon/log_message.rb
CHANGED
@@ -1,49 +1,47 @@
|
|
1
|
+
require "nokogiri"
|
2
|
+
|
1
3
|
module Savon
|
2
4
|
class LogMessage
|
3
5
|
|
4
|
-
def initialize(message, filters,
|
5
|
-
@message
|
6
|
-
@filters
|
7
|
-
@
|
6
|
+
def initialize(message, filters = [], pretty_print = false)
|
7
|
+
@message = message
|
8
|
+
@filters = filters
|
9
|
+
@pretty_print = pretty_print
|
8
10
|
end
|
9
11
|
|
10
12
|
def to_s
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
doc = apply_filter(doc) if filter?
|
15
|
-
doc.to_xml(pretty_options)
|
16
|
-
end
|
13
|
+
message_is_xml = @message =~ /^</
|
14
|
+
has_filters = @filters.any?
|
15
|
+
pretty_print = @pretty_print
|
17
16
|
|
18
|
-
|
17
|
+
return @message unless message_is_xml
|
18
|
+
return @message unless has_filters || pretty_print
|
19
19
|
|
20
|
-
|
21
|
-
|
20
|
+
document = Nokogiri.XML(@message)
|
21
|
+
document = apply_filter(document) if has_filters
|
22
|
+
document.to_xml(nokogiri_options)
|
22
23
|
end
|
23
24
|
|
24
|
-
|
25
|
-
@options[:pretty]
|
26
|
-
end
|
25
|
+
private
|
27
26
|
|
28
|
-
def apply_filter(
|
29
|
-
return
|
27
|
+
def apply_filter(document)
|
28
|
+
return document unless document.errors.empty?
|
30
29
|
|
31
30
|
@filters.each do |filter|
|
32
|
-
apply_filter!
|
31
|
+
apply_filter! document, filter
|
33
32
|
end
|
34
33
|
|
35
|
-
|
34
|
+
document
|
36
35
|
end
|
37
36
|
|
38
|
-
def apply_filter!(
|
39
|
-
|
37
|
+
def apply_filter!(document, filter)
|
38
|
+
document.xpath("//*[local-name()='#{filter}']").each do |node|
|
40
39
|
node.content = "***FILTERED***"
|
41
40
|
end
|
42
41
|
end
|
43
42
|
|
44
|
-
def
|
45
|
-
|
46
|
-
{ :indent => 2 }
|
43
|
+
def nokogiri_options
|
44
|
+
@pretty_print ? { :indent => 2 } : {}
|
47
45
|
end
|
48
46
|
|
49
47
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "savon/qualified_message"
|
2
|
+
require "gyoku"
|
3
|
+
|
4
|
+
module Savon
|
5
|
+
class Message
|
6
|
+
|
7
|
+
def initialize(operation_name, namespace_identifier, types, used_namespaces, message, element_form_default, key_converter)
|
8
|
+
@operation_name = operation_name
|
9
|
+
@namespace_identifier = namespace_identifier
|
10
|
+
@types = types
|
11
|
+
@used_namespaces = used_namespaces
|
12
|
+
|
13
|
+
@message = message
|
14
|
+
@element_form_default = element_form_default
|
15
|
+
@key_converter = key_converter
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_s
|
19
|
+
return @message.to_s unless @message.kind_of? Hash
|
20
|
+
|
21
|
+
if @element_form_default == :qualified
|
22
|
+
@message = QualifiedMessage.new(@types, @used_namespaces, @request_key_converter).to_hash(@message, [@operation_name.to_s])
|
23
|
+
end
|
24
|
+
|
25
|
+
gyoku_options = {
|
26
|
+
:element_form_default => @element_form_default,
|
27
|
+
:namespace => @namespace_identifier,
|
28
|
+
:key_converter => @key_converter
|
29
|
+
}
|
30
|
+
|
31
|
+
Gyoku.xml(@message, gyoku_options)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
data/lib/savon/mock.rb
ADDED
@@ -0,0 +1,70 @@
|
|
1
|
+
require "httpi"
|
2
|
+
|
3
|
+
module Savon
|
4
|
+
class MockExpectation
|
5
|
+
|
6
|
+
def initialize(operation_name)
|
7
|
+
@expected = { :operation_name => operation_name }
|
8
|
+
@actual = nil
|
9
|
+
end
|
10
|
+
|
11
|
+
def with(locals)
|
12
|
+
@expected[:message] = locals[:message]
|
13
|
+
self
|
14
|
+
end
|
15
|
+
|
16
|
+
def returns(response)
|
17
|
+
response = { :code => 200, :headers => {}, :body => response } if response.kind_of?(String)
|
18
|
+
@response = response
|
19
|
+
self
|
20
|
+
end
|
21
|
+
|
22
|
+
def actual(operation_name, builder, globals, locals)
|
23
|
+
@actual = {
|
24
|
+
:operation_name => operation_name,
|
25
|
+
:message => locals[:message]
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def verify!
|
30
|
+
unless @actual
|
31
|
+
raise ExpectationError, "Expected a request to the #{@expected[:operation_name].inspect} operation, " \
|
32
|
+
"but no request was executed."
|
33
|
+
end
|
34
|
+
|
35
|
+
verify_operation_name!
|
36
|
+
verify_message!
|
37
|
+
end
|
38
|
+
|
39
|
+
def response!
|
40
|
+
unless @response
|
41
|
+
raise ExpectationError, "This expectation was not set up with a response."
|
42
|
+
end
|
43
|
+
|
44
|
+
HTTPI::Response.new(@response[:code], @response[:headers], @response[:body])
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
def verify_operation_name!
|
50
|
+
unless @expected[:operation_name] == @actual[:operation_name]
|
51
|
+
raise ExpectationError, "Expected a request to the #{@expected[:operation_name].inspect} operation.\n" \
|
52
|
+
"Received a request to the #{@actual[:operation_name].inspect} operation instead."
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def verify_message!
|
57
|
+
unless @expected[:message] == @actual[:message]
|
58
|
+
expected_message = " with this message: #{@expected[:message].inspect}" if @expected[:message]
|
59
|
+
expected_message ||= " with no message."
|
60
|
+
|
61
|
+
actual_message = " with this message: #{@actual[:message].inspect}" if @actual[:message]
|
62
|
+
actual_message ||= " with no message."
|
63
|
+
|
64
|
+
raise ExpectationError, "Expected a request to the #{@expected[:operation_name].inspect} operation\n#{expected_message}\n" \
|
65
|
+
"Received a request to the #{@actual[:operation_name].inspect} operation\n#{actual_message}"
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require "savon/mock"
|
2
|
+
|
3
|
+
module Savon
|
4
|
+
module SpecHelper
|
5
|
+
|
6
|
+
class Interface
|
7
|
+
|
8
|
+
def mock!
|
9
|
+
Savon.observers << self
|
10
|
+
end
|
11
|
+
|
12
|
+
def unmock!
|
13
|
+
Savon.observers.clear
|
14
|
+
end
|
15
|
+
|
16
|
+
def expects(operation_name)
|
17
|
+
expectation = MockExpectation.new(operation_name)
|
18
|
+
expectations << expectation
|
19
|
+
expectation
|
20
|
+
end
|
21
|
+
|
22
|
+
def expectations
|
23
|
+
@expectations ||= []
|
24
|
+
end
|
25
|
+
|
26
|
+
def notify(operation_name, builder, globals, locals)
|
27
|
+
expectation = expectations.shift
|
28
|
+
|
29
|
+
if expectation
|
30
|
+
expectation.actual(operation_name, builder, globals, locals)
|
31
|
+
|
32
|
+
expectation.verify!
|
33
|
+
expectation.response!
|
34
|
+
else
|
35
|
+
raise ExpectationError, "Unexpected request to the #{operation_name.inspect} operation."
|
36
|
+
end
|
37
|
+
rescue ExpectationError
|
38
|
+
@expectations.clear
|
39
|
+
raise
|
40
|
+
end
|
41
|
+
|
42
|
+
def verify!
|
43
|
+
return if expectations.empty?
|
44
|
+
expectations.each(&:verify!)
|
45
|
+
rescue ExpectationError
|
46
|
+
@expectations.clear
|
47
|
+
raise
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
def savon
|
53
|
+
@savon ||= Interface.new
|
54
|
+
end
|
55
|
+
|
56
|
+
def verify_mocks_for_rspec
|
57
|
+
super if defined? super
|
58
|
+
savon.verify!
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
data/lib/savon/model.rb
CHANGED
@@ -1,8 +1,4 @@
|
|
1
1
|
module Savon
|
2
|
-
|
3
|
-
# = Savon::Model
|
4
|
-
#
|
5
|
-
# Model for SOAP service oriented applications.
|
6
2
|
module Model
|
7
3
|
|
8
4
|
def self.extended(base)
|
@@ -10,92 +6,74 @@ module Savon
|
|
10
6
|
end
|
11
7
|
|
12
8
|
def setup
|
13
|
-
|
14
|
-
|
9
|
+
class_operation_module
|
10
|
+
instance_operation_module
|
15
11
|
end
|
16
12
|
|
17
|
-
# Accepts one or more SOAP
|
18
|
-
# after the given
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
define_instance_action(action)
|
13
|
+
# Accepts one or more SOAP operations and generates both class and instance methods named
|
14
|
+
# after the given operations. Each generated method accepts an optional SOAP message Hash.
|
15
|
+
def operations(*operations)
|
16
|
+
operations.each do |operation|
|
17
|
+
define_class_operation(operation)
|
18
|
+
define_instance_operation(operation)
|
24
19
|
end
|
25
20
|
end
|
26
21
|
|
27
22
|
private
|
28
23
|
|
29
|
-
# Defines a class-level SOAP
|
30
|
-
def
|
31
|
-
|
32
|
-
def #{
|
33
|
-
client.
|
24
|
+
# Defines a class-level SOAP operation.
|
25
|
+
def define_class_operation(operation)
|
26
|
+
class_operation_module.module_eval %{
|
27
|
+
def #{operation.to_s.snakecase}(locals = {})
|
28
|
+
client.call #{operation.inspect}, locals
|
34
29
|
end
|
35
30
|
}
|
36
31
|
end
|
37
32
|
|
38
|
-
# Defines an instance-level SOAP
|
39
|
-
def
|
40
|
-
|
41
|
-
def #{
|
42
|
-
self.class.#{
|
33
|
+
# Defines an instance-level SOAP operation.
|
34
|
+
def define_instance_operation(operation)
|
35
|
+
instance_operation_module.module_eval %{
|
36
|
+
def #{operation.to_s.snakecase}(locals = {})
|
37
|
+
self.class.#{operation.to_s.snakecase} locals
|
43
38
|
end
|
44
39
|
}
|
45
40
|
end
|
46
41
|
|
47
42
|
# Class methods.
|
48
|
-
def
|
49
|
-
@
|
50
|
-
|
51
|
-
# Returns the memoized <tt>Savon::Client</tt>.
|
52
|
-
def client(&block)
|
53
|
-
@client ||= Savon::Client.new(&block)
|
54
|
-
end
|
55
|
-
|
56
|
-
# Sets the SOAP endpoint to the given +uri+.
|
57
|
-
def endpoint(uri)
|
58
|
-
client.wsdl.endpoint = uri
|
59
|
-
end
|
60
|
-
|
61
|
-
# Sets the target namespace.
|
62
|
-
def namespace(uri)
|
63
|
-
client.wsdl.namespace = uri
|
64
|
-
end
|
65
|
-
|
66
|
-
# Sets the WSDL document to the given +uri+.
|
67
|
-
def document(uri)
|
68
|
-
client.wsdl.document = uri
|
69
|
-
end
|
43
|
+
def class_operation_module
|
44
|
+
@class_operation_module ||= Module.new {
|
70
45
|
|
71
|
-
|
72
|
-
|
73
|
-
|
46
|
+
def client(globals = {})
|
47
|
+
@client ||= Savon::Client.new(globals)
|
48
|
+
rescue InitializationError
|
49
|
+
raise_initialization_error!
|
74
50
|
end
|
75
51
|
|
76
|
-
|
77
|
-
|
78
|
-
client.http.auth.basic(login, password)
|
52
|
+
def global(option, *value)
|
53
|
+
client.globals[option] = value
|
79
54
|
end
|
80
55
|
|
81
|
-
|
82
|
-
|
83
|
-
|
56
|
+
def raise_initialization_error!
|
57
|
+
raise InitializationError,
|
58
|
+
"Expected the model to be initialized with either a WSDL document or the SOAP endpoint and target namespace options.\n" \
|
59
|
+
"Make sure to setup the model by calling the .client class method before calling the .global method.\n\n" \
|
60
|
+
"client(wsdl: '/Users/me/project/service.wsdl') # to use a local WSDL document\n" \
|
61
|
+
"client(wsdl: 'http://example.com?wsdl') # to use a remote WSDL document\n" \
|
62
|
+
"client(endpoint: 'http://example.com', namespace: 'http://v1.example.com') # if you don't have a WSDL document"
|
84
63
|
end
|
85
64
|
|
86
|
-
|
65
|
+
}.tap { |mod| extend(mod) }
|
87
66
|
end
|
88
67
|
|
89
68
|
# Instance methods.
|
90
|
-
def
|
91
|
-
@
|
69
|
+
def instance_operation_module
|
70
|
+
@instance_operation_module ||= Module.new {
|
92
71
|
|
93
|
-
|
94
|
-
|
95
|
-
self.class.client(&block)
|
72
|
+
def client
|
73
|
+
self.class.client
|
96
74
|
end
|
97
75
|
|
98
|
-
|
76
|
+
}.tap { |mod| include(mod) }
|
99
77
|
end
|
100
78
|
|
101
79
|
end
|