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.
- 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
|