savon_with_adapter 2.4.1

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 (87) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +14 -0
  3. data/.travis.yml +11 -0
  4. data/.yardopts +6 -0
  5. data/CHANGELOG.md +1042 -0
  6. data/CONTRIBUTING.md +46 -0
  7. data/Gemfile +18 -0
  8. data/LICENSE +20 -0
  9. data/README.md +81 -0
  10. data/Rakefile +14 -0
  11. data/donate.png +0 -0
  12. data/lib/savon.rb +27 -0
  13. data/lib/savon/block_interface.rb +26 -0
  14. data/lib/savon/builder.rb +166 -0
  15. data/lib/savon/client.rb +89 -0
  16. data/lib/savon/core_ext/string.rb +29 -0
  17. data/lib/savon/header.rb +70 -0
  18. data/lib/savon/http_error.rb +27 -0
  19. data/lib/savon/log_message.rb +48 -0
  20. data/lib/savon/message.rb +35 -0
  21. data/lib/savon/mock.rb +5 -0
  22. data/lib/savon/mock/expectation.rb +71 -0
  23. data/lib/savon/mock/spec_helper.rb +62 -0
  24. data/lib/savon/model.rb +80 -0
  25. data/lib/savon/operation.rb +127 -0
  26. data/lib/savon/options.rb +336 -0
  27. data/lib/savon/qualified_message.rb +49 -0
  28. data/lib/savon/request.rb +89 -0
  29. data/lib/savon/request_logger.rb +48 -0
  30. data/lib/savon/response.rb +112 -0
  31. data/lib/savon/soap_fault.rb +48 -0
  32. data/lib/savon/version.rb +3 -0
  33. data/savon.gemspec +52 -0
  34. data/spec/fixtures/gzip/message.gz +0 -0
  35. data/spec/fixtures/response/another_soap_fault.xml +14 -0
  36. data/spec/fixtures/response/authentication.xml +14 -0
  37. data/spec/fixtures/response/header.xml +13 -0
  38. data/spec/fixtures/response/list.xml +18 -0
  39. data/spec/fixtures/response/multi_ref.xml +39 -0
  40. data/spec/fixtures/response/soap_fault.xml +8 -0
  41. data/spec/fixtures/response/soap_fault12.xml +18 -0
  42. data/spec/fixtures/response/taxcloud.xml +1 -0
  43. data/spec/fixtures/ssl/client_cert.pem +16 -0
  44. data/spec/fixtures/ssl/client_encrypted_key.pem +30 -0
  45. data/spec/fixtures/ssl/client_encrypted_key_cert.pem +24 -0
  46. data/spec/fixtures/ssl/client_key.pem +15 -0
  47. data/spec/fixtures/wsdl/authentication.xml +63 -0
  48. data/spec/fixtures/wsdl/betfair.xml +2981 -0
  49. data/spec/fixtures/wsdl/edialog.xml +15416 -0
  50. data/spec/fixtures/wsdl/interhome.xml +2137 -0
  51. data/spec/fixtures/wsdl/lower_camel.xml +52 -0
  52. data/spec/fixtures/wsdl/multiple_namespaces.xml +92 -0
  53. data/spec/fixtures/wsdl/multiple_types.xml +60 -0
  54. data/spec/fixtures/wsdl/taxcloud.xml +934 -0
  55. data/spec/fixtures/wsdl/team_software.xml +1 -0
  56. data/spec/fixtures/wsdl/vies.xml +176 -0
  57. data/spec/fixtures/wsdl/wasmuth.xml +153 -0
  58. data/spec/integration/centra_spec.rb +72 -0
  59. data/spec/integration/email_example_spec.rb +32 -0
  60. data/spec/integration/random_quote_spec.rb +23 -0
  61. data/spec/integration/ratp_example_spec.rb +28 -0
  62. data/spec/integration/stockquote_example_spec.rb +28 -0
  63. data/spec/integration/support/application.rb +82 -0
  64. data/spec/integration/support/server.rb +84 -0
  65. data/spec/integration/temperature_example_spec.rb +46 -0
  66. data/spec/integration/zipcode_example_spec.rb +42 -0
  67. data/spec/savon/builder_spec.rb +86 -0
  68. data/spec/savon/client_spec.rb +198 -0
  69. data/spec/savon/core_ext/string_spec.rb +37 -0
  70. data/spec/savon/features/message_tag_spec.rb +61 -0
  71. data/spec/savon/http_error_spec.rb +49 -0
  72. data/spec/savon/log_message_spec.rb +33 -0
  73. data/spec/savon/message_spec.rb +40 -0
  74. data/spec/savon/mock_spec.rb +157 -0
  75. data/spec/savon/model_spec.rb +154 -0
  76. data/spec/savon/observers_spec.rb +92 -0
  77. data/spec/savon/operation_spec.rb +211 -0
  78. data/spec/savon/options_spec.rb +772 -0
  79. data/spec/savon/request_spec.rb +493 -0
  80. data/spec/savon/response_spec.rb +258 -0
  81. data/spec/savon/soap_fault_spec.rb +126 -0
  82. data/spec/spec_helper.rb +30 -0
  83. data/spec/support/endpoint.rb +25 -0
  84. data/spec/support/fixture.rb +39 -0
  85. data/spec/support/integration.rb +9 -0
  86. data/spec/support/stdout.rb +25 -0
  87. metadata +310 -0
@@ -0,0 +1,29 @@
1
+
2
+ module Savon
3
+ module CoreExt
4
+ module String
5
+
6
+ def self.included(base)
7
+ unless "savon".respond_to?(:snakecase)
8
+ base.send(:include, Extension)
9
+ end
10
+ end
11
+
12
+ module Extension
13
+ def snakecase
14
+ str = dup
15
+ str.gsub! /::/, '/'
16
+ str.gsub! /([A-Z]+)([A-Z][a-z])/, '\1_\2'
17
+ str.gsub! /([a-z\d])([A-Z])/, '\1_\2'
18
+ str.tr! ".", "_"
19
+ str.tr! "-", "_"
20
+ str.downcase!
21
+ str
22
+ end
23
+ end
24
+
25
+ end
26
+ end
27
+ end
28
+
29
+ String.send :include, Savon::CoreExt::String
@@ -0,0 +1,70 @@
1
+ require "akami"
2
+ require "gyoku"
3
+
4
+ module Savon
5
+ class Header
6
+
7
+ def initialize(globals, locals)
8
+ @gyoku_options = { :key_converter => globals[:convert_request_keys_to] }
9
+
10
+ @wsse_auth = globals[:wsse_auth]
11
+ @wsse_timestamp = globals[:wsse_timestamp]
12
+
13
+ @global_header = globals[:soap_header]
14
+ @local_header = locals[:soap_header]
15
+
16
+ @header = build
17
+ end
18
+
19
+ attr_reader :local_header, :global_header, :gyoku_options,
20
+ :wsse_auth, :wsse_timestamp
21
+
22
+ def empty?
23
+ @header.empty?
24
+ end
25
+
26
+ def to_s
27
+ @header
28
+ end
29
+
30
+ private
31
+
32
+ def build
33
+ build_header + build_wsse_header
34
+ end
35
+
36
+ def build_header
37
+ header =
38
+ if global_header.kind_of?(Hash) && local_header.kind_of?(Hash)
39
+ global_header.merge(local_header)
40
+ elsif local_header
41
+ local_header
42
+ else
43
+ global_header
44
+ end
45
+
46
+ convert_to_xml(header)
47
+ end
48
+
49
+ def build_wsse_header
50
+ wsse_header = akami
51
+ wsse_header.respond_to?(:to_xml) ? wsse_header.to_xml : ""
52
+ end
53
+
54
+ def convert_to_xml(hash_or_string)
55
+ if hash_or_string.kind_of? Hash
56
+ Gyoku.xml(hash_or_string, gyoku_options)
57
+ else
58
+ hash_or_string.to_s
59
+ end
60
+ end
61
+
62
+ def akami
63
+ wsse = Akami.wsse
64
+ wsse.credentials(*wsse_auth) if wsse_auth
65
+ wsse.timestamp = wsse_timestamp if wsse_timestamp
66
+ wsse
67
+ end
68
+
69
+ end
70
+ 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
@@ -0,0 +1,48 @@
1
+ require "nokogiri"
2
+
3
+ module Savon
4
+ class LogMessage
5
+
6
+ def initialize(message, filters = [], pretty_print = false)
7
+ @message = message
8
+ @filters = filters
9
+ @pretty_print = pretty_print
10
+ end
11
+
12
+ def to_s
13
+ message_is_xml = @message =~ /^</
14
+ has_filters = @filters.any?
15
+ pretty_print = @pretty_print
16
+
17
+ return @message unless message_is_xml
18
+ return @message unless has_filters || pretty_print
19
+
20
+ document = Nokogiri.XML(@message)
21
+ document = apply_filter(document) if has_filters
22
+ document.to_xml(nokogiri_options)
23
+ end
24
+
25
+ private
26
+
27
+ def apply_filter(document)
28
+ return document unless document.errors.empty?
29
+
30
+ @filters.each do |filter|
31
+ apply_filter! document, filter
32
+ end
33
+
34
+ document
35
+ end
36
+
37
+ def apply_filter!(document, filter)
38
+ document.xpath("//*[local-name()='#{filter}']").each do |node|
39
+ node.content = "***FILTERED***"
40
+ end
41
+ end
42
+
43
+ def nokogiri_options
44
+ @pretty_print ? { :indent => 2 } : {}
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,35 @@
1
+ require "savon/qualified_message"
2
+ require "gyoku"
3
+
4
+ module Savon
5
+ class Message
6
+
7
+ def initialize(message_tag, namespace_identifier, types, used_namespaces, message, element_form_default, key_converter)
8
+ @message_tag = message_tag
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, @key_converter).to_hash(@message, [@message_tag.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 < StandardError; end
3
+ end
4
+
5
+ require "savon/mock/expectation"
@@ -0,0 +1,71 @@
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
+ return if @expected[:message] == :any
58
+ unless @expected[:message] == @actual[:message]
59
+ expected_message = " with this message: #{@expected[:message].inspect}" if @expected[:message]
60
+ expected_message ||= " with no message."
61
+
62
+ actual_message = " with this message: #{@actual[:message].inspect}" if @actual[:message]
63
+ actual_message ||= " with no message."
64
+
65
+ raise ExpectationError, "Expected a request to the #{@expected[:operation_name].inspect} operation\n#{expected_message}\n" \
66
+ "Received a request to the #{@actual[:operation_name].inspect} operation\n#{actual_message}"
67
+ end
68
+ end
69
+
70
+ end
71
+ 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
@@ -0,0 +1,80 @@
1
+ module Savon
2
+ module Model
3
+
4
+ def self.extended(base)
5
+ base.setup
6
+ end
7
+
8
+ def setup
9
+ class_operation_module
10
+ instance_operation_module
11
+ end
12
+
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)
19
+ end
20
+ end
21
+
22
+ private
23
+
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
29
+ end
30
+ }
31
+ end
32
+
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
38
+ end
39
+ }
40
+ end
41
+
42
+ # Class methods.
43
+ def class_operation_module
44
+ @class_operation_module ||= Module.new {
45
+
46
+ def client(globals = {})
47
+ @client ||= Savon::Client.new(globals)
48
+ rescue InitializationError
49
+ raise_initialization_error!
50
+ end
51
+
52
+ def global(option, *value)
53
+ client.globals[option] = value
54
+ end
55
+
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"
63
+ end
64
+
65
+ }.tap { |mod| extend(mod) }
66
+ end
67
+
68
+ # Instance methods.
69
+ def instance_operation_module
70
+ @instance_operation_module ||= Module.new {
71
+
72
+ def client
73
+ self.class.client
74
+ end
75
+
76
+ }.tap { |mod| include(mod) }
77
+ end
78
+
79
+ end
80
+ end
@@ -0,0 +1,127 @@
1
+ require "savon/options"
2
+ require "savon/block_interface"
3
+ require "savon/request"
4
+ require "savon/builder"
5
+ require "savon/response"
6
+ require "savon/request_logger"
7
+
8
+ module Savon
9
+ class Operation
10
+
11
+ def self.create(operation_name, wsdl, globals)
12
+ if wsdl.document?
13
+ ensure_name_is_symbol! operation_name
14
+ ensure_exists! operation_name, wsdl
15
+ end
16
+
17
+ new(operation_name, wsdl, globals)
18
+ end
19
+
20
+ def self.ensure_exists!(operation_name, wsdl)
21
+ unless wsdl.soap_actions.include? operation_name
22
+ raise UnknownOperationError, "Unable to find SOAP operation: #{operation_name.inspect}\n" \
23
+ "Operations provided by your service: #{wsdl.soap_actions.inspect}"
24
+ end
25
+ end
26
+
27
+ def self.ensure_name_is_symbol!(operation_name)
28
+ unless operation_name.kind_of? Symbol
29
+ raise ArgumentError, "Expected the first parameter (the name of the operation to call) to be a symbol\n" \
30
+ "Actual: #{operation_name.inspect} (#{operation_name.class})"
31
+ end
32
+ end
33
+
34
+ def initialize(name, wsdl, globals)
35
+ @name = name
36
+ @wsdl = wsdl
37
+ @globals = globals
38
+
39
+ @logger = RequestLogger.new(globals)
40
+ end
41
+
42
+ def build(locals = {}, &block)
43
+ set_locals(locals, block)
44
+ Builder.new(@name, @wsdl, @globals, @locals)
45
+ end
46
+
47
+ def call(locals = {}, &block)
48
+ builder = build(locals, &block)
49
+
50
+ response = Savon.notify_observers(@name, builder, @globals, @locals)
51
+ response ||= call_with_logging build_request(builder)
52
+
53
+ raise_expected_httpi_response! unless response.kind_of?(HTTPI::Response)
54
+
55
+ create_response(response)
56
+ end
57
+
58
+ private
59
+
60
+ def create_response(response)
61
+ if multipart_supported?
62
+ Multipart::Response.new(response, @globals, @locals)
63
+ else
64
+ Response.new(response, @globals, @locals)
65
+ end
66
+ end
67
+
68
+ def multipart_supported?
69
+ return false unless @globals[:multipart] || @locals[:multipart]
70
+
71
+ if Savon.const_defined? :Multipart
72
+ true
73
+ else
74
+ raise 'Unable to find Savon::Multipart. Make sure the savon-multipart gem is installed and loaded.'
75
+ end
76
+ end
77
+
78
+ def set_locals(locals, block)
79
+ locals = LocalOptions.new(locals)
80
+ BlockInterface.new(locals).evaluate(block) if block
81
+
82
+ @locals = locals
83
+ end
84
+
85
+ def call_with_logging(request)
86
+ @logger.log(request) { HTTPI.post(request, @globals[:adapter]) }
87
+ end
88
+
89
+ def build_request(builder)
90
+ request = SOAPRequest.new(@globals).build(
91
+ :soap_action => soap_action,
92
+ :cookies => @locals[:cookies]
93
+ )
94
+
95
+ request.url = endpoint
96
+ request.body = builder.to_s
97
+
98
+ # TODO: could HTTPI do this automatically in case the header
99
+ # was not specified manually? [dh, 2013-01-04]
100
+ request.headers["Content-Length"] = request.body.bytesize.to_s
101
+
102
+ request
103
+ end
104
+
105
+ def soap_action
106
+ # soap_action explicitly set to something falsy
107
+ return if @locals.include?(:soap_action) && !@locals[:soap_action]
108
+
109
+ # get the soap_action from local options
110
+ soap_action = @locals[:soap_action]
111
+ # with no local option, but a wsdl, ask it for the soap_action
112
+ soap_action ||= @wsdl.soap_action(@name.to_sym) if @wsdl.document?
113
+ # if there is no soap_action up to this point, fallback to a simple default
114
+ soap_action ||= Gyoku.xml_tag(@name, :key_converter => @globals[:convert_request_keys_to])
115
+ end
116
+
117
+ def endpoint
118
+ @globals[:endpoint] || @wsdl.endpoint
119
+ end
120
+
121
+ def raise_expected_httpi_response!
122
+ raise Error, "Observers need to return an HTTPI::Response to mock " \
123
+ "the request or nil to execute the request."
124
+ end
125
+
126
+ end
127
+ end