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
@@ -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
@@ -1,49 +1,47 @@
1
+ require "nokogiri"
2
+
1
3
  module Savon
2
4
  class LogMessage
3
5
 
4
- def initialize(message, filters, options = {})
5
- @message = message
6
- @filters = filters
7
- @options = options
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
- return @message unless pretty? || filter?
12
-
13
- doc = Nokogiri.XML(@message)
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
- private
17
+ return @message unless message_is_xml
18
+ return @message unless has_filters || pretty_print
19
19
 
20
- def filter?
21
- @options[:filter] && @filters.any?
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
- def pretty?
25
- @options[:pretty]
26
- end
25
+ private
27
26
 
28
- def apply_filter(doc)
29
- return doc unless doc.errors.empty?
27
+ def apply_filter(document)
28
+ return document unless document.errors.empty?
30
29
 
31
30
  @filters.each do |filter|
32
- apply_filter!(doc, filter)
31
+ apply_filter! document, filter
33
32
  end
34
33
 
35
- doc
34
+ document
36
35
  end
37
36
 
38
- def apply_filter!(doc, filter)
39
- doc.xpath("//*[local-name()='#{filter}']").each do |node|
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 pretty_options
45
- return {} unless pretty?
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
@@ -0,0 +1,5 @@
1
+ module Savon
2
+ class ExpectationError < Error; end
3
+ end
4
+
5
+ require "savon/mock/expectation"
@@ -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
@@ -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
- class_action_module
14
- instance_action_module
9
+ class_operation_module
10
+ instance_operation_module
15
11
  end
16
12
 
17
- # Accepts one or more SOAP actions and generates both class and instance methods named
18
- # after the given actions. Each generated method accepts an optional SOAP body Hash and
19
- # a block to be passed to <tt>Savon::Client#request</tt> and executes a SOAP request.
20
- def actions(*actions)
21
- actions.each do |action|
22
- define_class_action(action)
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 action method.
30
- def define_class_action(action)
31
- class_action_module.module_eval %{
32
- def #{action.to_s.snakecase}(body = nil, &block)
33
- client.request :wsdl, #{action.inspect}, :body => body, &block
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 action method.
39
- def define_instance_action(action)
40
- instance_action_module.module_eval %{
41
- def #{action.to_s.snakecase}(body = nil, &block)
42
- self.class.#{action.to_s.snakecase} body, &block
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 class_action_module
49
- @class_action_module ||= Module.new do
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
- # Sets the HTTP headers.
72
- def headers(headers)
73
- client.http.headers = headers
46
+ def client(globals = {})
47
+ @client ||= Savon::Client.new(globals)
48
+ rescue InitializationError
49
+ raise_initialization_error!
74
50
  end
75
51
 
76
- # Sets basic auth +login+ and +password+.
77
- def basic_auth(login, password)
78
- client.http.auth.basic(login, password)
52
+ def global(option, *value)
53
+ client.globals[option] = value
79
54
  end
80
55
 
81
- # Sets WSSE auth credentials.
82
- def wsse_auth(*args)
83
- client.wsse.credentials(*args)
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
- end.tap { |mod| extend(mod) }
65
+ }.tap { |mod| extend(mod) }
87
66
  end
88
67
 
89
68
  # Instance methods.
90
- def instance_action_module
91
- @instance_action_module ||= Module.new do
69
+ def instance_operation_module
70
+ @instance_operation_module ||= Module.new {
92
71
 
93
- # Returns the <tt>Savon::Client</tt> from the class instance.
94
- def client(&block)
95
- self.class.client(&block)
72
+ def client
73
+ self.class.client
96
74
  end
97
75
 
98
- end.tap { |mod| include(mod) }
76
+ }.tap { |mod| include(mod) }
99
77
  end
100
78
 
101
79
  end