savon 2.16.0 → 2.17.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/lib/savon/model.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module Savon
3
4
  module Model
4
-
5
5
  def self.extended(base)
6
6
  base.setup
7
7
  end
@@ -32,7 +32,7 @@ module Savon
32
32
  def #{StringUtils.snakecase(operation.to_s)}(locals = {})
33
33
  client.call #{operation.inspect}, locals
34
34
  end
35
- }
35
+ }, __FILE__, __LINE__ - 4 # -4 points to the line where the eval string starts
36
36
  end
37
37
 
38
38
  # Defines an instance-level SOAP operation.
@@ -41,13 +41,12 @@ module Savon
41
41
  def #{StringUtils.snakecase(operation.to_s)}(locals = {})
42
42
  self.class.#{StringUtils.snakecase(operation.to_s)} locals
43
43
  end
44
- }
44
+ }, __FILE__, __LINE__ - 4 # -4 points to the line where the eval string starts
45
45
  end
46
46
 
47
47
  # Class methods.
48
48
  def class_operation_module
49
- @class_operation_module ||= Module.new {
50
-
49
+ @class_operation_module ||= Module.new do
51
50
  def client(globals = {})
52
51
  @client ||= Savon::Client.new(globals)
53
52
  rescue InitializationError
@@ -60,26 +59,22 @@ module Savon
60
59
 
61
60
  def raise_initialization_error!
62
61
  raise InitializationError,
63
- "Expected the model to be initialized with either a WSDL document or the SOAP endpoint and target namespace options.\n" \
64
- "Make sure to setup the model by calling the .client class method before calling the .global method.\n\n" \
65
- "client(wsdl: '/Users/me/project/service.wsdl') # to use a local WSDL document\n" \
66
- "client(wsdl: 'http://example.com?wsdl') # to use a remote WSDL document\n" \
67
- "client(endpoint: 'http://example.com', namespace: 'http://v1.example.com') # if you don't have a WSDL document"
62
+ "Expected the model to be initialized with either a WSDL document or the SOAP endpoint and target namespace options.\n" \
63
+ "Make sure to setup the model by calling the .client class method before calling the .global method.\n\n" \
64
+ "client(wsdl: '/Users/me/project/service.wsdl') # to use a local WSDL document\n" \
65
+ "client(wsdl: 'http://example.com?wsdl') # to use a remote WSDL document\n" \
66
+ "client(endpoint: 'http://example.com', namespace: 'http://v1.example.com') # if you don't have a WSDL document"
68
67
  end
69
-
70
- }.tap { |mod| extend(mod) }
68
+ end.tap { |mod| extend(mod) }
71
69
  end
72
70
 
73
71
  # Instance methods.
74
72
  def instance_operation_module
75
- @instance_operation_module ||= Module.new {
76
-
73
+ @instance_operation_module ||= Module.new do
77
74
  def client
78
75
  self.class.client
79
76
  end
80
-
81
- }.tap { |mod| include(mod) }
77
+ end.tap { |mod| include(mod) }
82
78
  end
83
-
84
79
  end
85
80
  end
@@ -1,28 +1,42 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  require "savon/options"
3
4
  require "savon/block_interface"
4
- require "savon/request"
5
5
  require "savon/builder"
6
6
  require "savon/response"
7
- require "savon/request_logger"
8
7
  require "savon/http_error"
8
+ require "savon/transport/httpi"
9
+ require "savon/transport/faraday"
9
10
  require "mail"
10
11
 
11
12
  module Savon
13
+ # Represents a single named SOAP operation.
14
+ #
15
+ # Bridges the SOAP layer (envelope building, action headers, multipart) and the
16
+ # transport layer (execution, logging). Knows nothing about transport internals
17
+ # such as proxy, SSL, or auth.
12
18
  class Operation
13
-
19
+ # SOAP Content-Type values indexed by SOAP version.
20
+ # SOAP 1.1 §6 (HTTP binding), SOAP 1.2 Part 2 §7.1.4 (HTTP media type)
21
+ CONTENT_TYPE = {
22
+ 1 => "text/xml;charset=%s",
23
+ 2 => "application/soap+xml;charset=%s"
24
+ }.freeze
25
+
26
+ # Maps SOAP version to the base MIME type used in multipart requests.
27
+ # RFC 2387 §3.1 (multipart/related Content-Type parameter)
14
28
  SOAP_REQUEST_TYPE = {
15
29
  1 => "text/xml",
16
30
  2 => "application/soap+xml"
17
- }
31
+ }.freeze
18
32
 
19
- def self.create(operation_name, wsdl, globals)
33
+ def self.create(operation_name, wsdl, globals, transport)
20
34
  if wsdl.document?
21
35
  ensure_name_is_symbol! operation_name
22
36
  ensure_exists! operation_name, wsdl
23
37
  end
24
38
 
25
- new(operation_name, wsdl, globals)
39
+ new(operation_name, wsdl, globals, transport)
26
40
  end
27
41
 
28
42
  def self.ensure_exists!(operation_name, wsdl)
@@ -31,22 +45,21 @@ module Savon
31
45
  "Operations provided by your service: #{wsdl.soap_actions.inspect}"
32
46
  end
33
47
  rescue Wasabi::Resolver::HTTPError => e
34
- raise HTTPError.new(e.response)
48
+ raise HTTPError, e.response
35
49
  end
36
50
 
37
51
  def self.ensure_name_is_symbol!(operation_name)
38
- unless operation_name.kind_of? Symbol
39
- raise ArgumentError, "Expected the first parameter (the name of the operation to call) to be a symbol\n" \
40
- "Actual: #{operation_name.inspect} (#{operation_name.class})"
41
- end
42
- end
52
+ return if operation_name.is_a? Symbol
43
53
 
44
- def initialize(name, wsdl, globals)
45
- @name = name
46
- @wsdl = wsdl
47
- @globals = globals
54
+ raise ArgumentError, "Expected the first parameter (the name of the operation to call) to be a symbol\n" \
55
+ "Actual: #{operation_name.inspect} (#{operation_name.class})"
56
+ end
48
57
 
49
- @logger = RequestLogger.new(globals)
58
+ def initialize(name, wsdl, globals, transport)
59
+ @name = name
60
+ @wsdl = wsdl
61
+ @globals = globals
62
+ @transport = transport
50
63
  end
51
64
 
52
65
  def build(locals = {}, &block)
@@ -54,20 +67,37 @@ module Savon
54
67
  Builder.new(@name, @wsdl, @globals, @locals)
55
68
  end
56
69
 
70
+ # Executes the SOAP operation and returns a Savon::Response.
71
+ #
72
+ # Observer short-circuit: if any registered observer returns a
73
+ # Transport::Response (or legacy HTTPI::Response), the HTTP call
74
+ # is skipped and that response is used directly.
57
75
  def call(locals = {}, &block)
58
- builder = build(locals, &block)
59
-
76
+ builder = build(locals, &block)
60
77
  response = Savon.notify_observers(@name, builder, @globals, @locals)
61
- response ||= call_with_logging build_request(builder)
62
78
 
63
- raise_expected_httpi_response! unless response.kind_of?(HTTPI::Response)
79
+ response =
80
+ if response.nil?
81
+ @transport.post(endpoint.to_s, soap_headers(builder), builder.to_s, @locals)
82
+ else
83
+ normalize_observer_response(response)
84
+ end
64
85
 
65
86
  create_response(response)
66
87
  end
67
88
 
89
+ # Builds and returns the HTTPI::Request that would be sent for this
90
+ # operation, without executing it. Useful for inspection and debugging.
91
+ # Not supported with transport: :faraday.
68
92
  def request(locals = {}, &block)
93
+ if @globals[:transport] == :faraday
94
+ raise ArgumentError, "#request returns an HTTPI::Request and is not supported " \
95
+ "with transport: :faraday. Use client.faraday to configure " \
96
+ "the connection"
97
+ end
98
+
69
99
  builder = build(locals, &block)
70
- build_request(builder)
100
+ @transport.to_httpi_request(endpoint.to_s, soap_headers(builder), builder.to_s, @locals)
71
101
  end
72
102
 
73
103
  private
@@ -83,37 +113,33 @@ module Savon
83
113
  @locals = locals
84
114
  end
85
115
 
86
- def call_with_logging(request)
87
- @logger.log(request) { HTTPI.post(request, @globals[:adapter]) }
88
- end
89
-
90
- def build_request(builder)
91
- @locals[:soap_action] ||= soap_action
92
- @globals[:endpoint] ||= endpoint
93
-
94
- request = SOAPRequest.new(@globals).build(
95
- :soap_action => soap_action,
96
- :cookies => @locals[:cookies],
97
- :headers => @locals[:headers]
98
- )
99
-
100
- request.url = endpoint
101
- request.body = builder.to_s
116
+ # Assembles the SOAP-level request headers for the given builder.
117
+ #
118
+ # Our responsibility regardless of transport:
119
+ # * Content-Type (SOAP 1.1 §6 / SOAP 1.2 Part 2 §7.1.4)
120
+ # * SOAPAction (SOAP 1.1 §6.1.1)
121
+ # * Multipart Content-Type (RFC 2387), MIME-Version (RFC 2045 §4), Accept-Encoding (RFC 9110 §12.5.3)
122
+ def soap_headers(builder)
123
+ headers = {}
102
124
 
103
125
  if builder.multipart
104
- request.gzip
105
- request.headers["Content-Type"] = ["multipart/related",
106
- "type=\"#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}\"",
107
- "start=\"#{builder.multipart[:start]}\"",
108
- "boundary=\"#{builder.multipart[:multipart_boundary]}\""].join("; ")
109
- request.headers["MIME-Version"] = "1.0"
126
+ # RFC 2387 §3 (multipart/related) - SOAP envelope is the root body part
127
+ headers["Content-Type"] = [
128
+ "multipart/related",
129
+ "type=\"#{SOAP_REQUEST_TYPE[@globals[:soap_version]]}\"",
130
+ "start=\"#{builder.multipart[:start]}\"",
131
+ "boundary=\"#{builder.multipart[:multipart_boundary]}\""
132
+ ].join("; ")
133
+ headers["MIME-Version"] = "1.0"
134
+ headers["Accept-Encoding"] = "gzip,deflate"
135
+ else
136
+ headers["Content-Type"] = CONTENT_TYPE[@globals[:soap_version]] % @globals[:encoding]
110
137
  end
111
138
 
112
- # TODO: could HTTPI do this automatically in case the header
113
- # was not specified manually? [dh, 2013-01-04]
114
- request.headers["Content-Length"] = request.body.bytesize.to_s
139
+ action = soap_action
140
+ headers["SOAPAction"] = %("#{action}") if action
115
141
 
116
- request
142
+ headers
117
143
  end
118
144
 
119
145
  def soap_action
@@ -122,10 +148,10 @@ module Savon
122
148
 
123
149
  # get the soap_action from local options
124
150
  @locals[:soap_action] ||
125
- # with no local option, but a wsdl, ask it for the soap_action
126
- @wsdl.document? && @wsdl.soap_action(@name.to_sym) ||
127
- # if there is no soap_action up to this point, fallback to a simple default
128
- Gyoku.xml_tag(@name, :key_converter => @globals[:convert_request_keys_to])
151
+ # with no local option, but a wsdl, ask it for the soap_action
152
+ @wsdl.document? && @wsdl.soap_action(@name.to_sym) ||
153
+ # if there is no soap_action up to this point, fallback to a simple default
154
+ Gyoku.xml_tag(@name, key_converter: @globals[:convert_request_keys_to])
129
155
  end
130
156
 
131
157
  def endpoint
@@ -138,10 +164,21 @@ module Savon
138
164
  end
139
165
  end
140
166
 
141
- def raise_expected_httpi_response!
142
- raise Error, "Observers need to return an HTTPI::Response to mock " \
143
- "the request or nil to execute the request."
144
- end
167
+ # Normalizes an observer return value into a Transport::Response.
168
+ #
169
+ # Accepts Transport::Response directly (current contract), wraps
170
+ # HTTPI::Response with a deprecation warning (legacy observer support),
171
+ # and raises on anything else.
172
+ def normalize_observer_response(response)
173
+ return response if response.is_a?(Transport::Response)
174
+
175
+ if response.is_a?(HTTPI::Response)
176
+ warn "Observers returning HTTPI::Response is deprecated - return Savon::Transport::Response instead."
177
+ return Transport::Response.from_httpi(response)
178
+ end
145
179
 
180
+ raise Error, "Observers need to return a Savon::Transport::Response " \
181
+ "to mock the request or nil to execute the request."
182
+ end
146
183
  end
147
184
  end