savon 2.17.4 → 3.0.0.rc1

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/client.rb CHANGED
@@ -1,26 +1,19 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  require "savon/operation"
4
- require "savon/transport/httpi"
5
- require "savon/transport/faraday"
3
+ require "savon/request"
6
4
  require "savon/options"
7
5
  require "savon/block_interface"
8
6
  require "wasabi"
9
7
 
10
8
  module Savon
11
- # The main entry point for Savon.
12
- #
13
- # Holds global configuration, owns the WSDL document, and dispatches
14
- # named operations. A single Client instance is typically shared across
15
- # multiple calls to the same service.
16
9
  class Client
10
+
17
11
  def initialize(globals = {}, &block)
18
- unless globals.is_a? Hash
12
+ unless globals.kind_of? Hash
19
13
  raise_version1_initialize_error! globals
20
14
  end
21
15
 
22
16
  set_globals(globals, block)
23
- @globals.validate_transport!
24
17
 
25
18
  unless wsdl_or_endpoint_and_namespace_specified?
26
19
  raise_initialization_error!
@@ -31,25 +24,13 @@ module Savon
31
24
 
32
25
  attr_reader :globals, :wsdl
33
26
 
34
- # Returns the memoized Faraday::Connection for this client.
35
- # Callers use this to configure middleware, SSL, auth, timeouts, and any
36
- # other transport-level concern before making calls.
37
- # Raises ArgumentError if transport is not :faraday.
38
- def faraday
39
- unless @globals[:transport] == :faraday
40
- raise ArgumentError, "client.faraday is only available when transport: :faraday is set"
41
- end
42
-
43
- @faraday ||= ::Faraday.new
44
- end
45
-
46
27
  def operations
47
28
  raise_missing_wsdl_error! unless @wsdl.document?
48
29
  @wsdl.soap_actions
49
30
  end
50
31
 
51
32
  def operation(operation_name)
52
- Operation.create(operation_name, @wsdl, @globals, build_transport)
33
+ Operation.create(operation_name, @wsdl, @globals)
53
34
  end
54
35
 
55
36
  def call(operation_name, locals = {}, &block)
@@ -67,15 +48,6 @@ module Savon
67
48
 
68
49
  private
69
50
 
70
- # Builds the transport for a single operation.
71
- def build_transport
72
- if @globals[:transport] == :faraday
73
- Transport::Faraday.new(faraday, @globals)
74
- else
75
- Transport::HTTPI.new(@globals)
76
- end
77
- end
78
-
79
51
  def set_globals(globals, block)
80
52
  globals = GlobalOptions.new(globals)
81
53
  BlockInterface.new(globals).evaluate(block) if block
@@ -86,16 +58,12 @@ module Savon
86
58
  def build_wsdl_document
87
59
  @wsdl = Wasabi::Document.new
88
60
 
89
- @wsdl.document = @globals[:wsdl] if @globals.include? :wsdl
90
- @wsdl.endpoint = @globals[:endpoint] if @globals.include? :endpoint
91
- @wsdl.namespace = @globals[:namespace] if @globals.include? :namespace
61
+ @wsdl.document = @globals[:wsdl] if @globals.include? :wsdl
62
+ @wsdl.endpoint = @globals[:endpoint] if @globals.include? :endpoint
63
+ @wsdl.namespace = @globals[:namespace] if @globals.include? :namespace
64
+ @wsdl.adapter = @globals[:adapter] if @globals.include? :adapter
92
65
 
93
- if @globals[:transport] == :faraday
94
- @wsdl.request = faraday
95
- else
96
- @wsdl.adapter = @globals[:adapter] if @globals.include? :adapter
97
- @wsdl.request = Transport::HTTPI.new(@globals).wsdl_request
98
- end
66
+ @wsdl.request = WSDLRequest.new(@globals).build
99
67
  end
100
68
 
101
69
  def wsdl_or_endpoint_and_namespace_specified?
@@ -104,9 +72,9 @@ module Savon
104
72
 
105
73
  def raise_version1_initialize_error!(object)
106
74
  raise InitializationError,
107
- "Some code tries to initialize Savon with the #{object.inspect} (#{object.class}) \n" \
108
- "Savon 2 expects a Hash of options for creating a new client and executing requests.\n" \
109
- "Please read the updated documentation for version 2: http://savonrb.com/version2.html"
75
+ "Some code tries to initialize Savon with the #{object.inspect} (#{object.class}) \n" \
76
+ "Savon 2 expects a Hash of options for creating a new client and executing requests.\n" \
77
+ "Please read the updated documentation for version 2: http://savonrb.com/version2.html"
110
78
  end
111
79
 
112
80
  def raise_initialization_error!
@@ -120,5 +88,6 @@ module Savon
120
88
  def raise_missing_wsdl_error!
121
89
  raise "Unable to inspect the service without a WSDL document."
122
90
  end
91
+
123
92
  end
124
93
  end
data/lib/savon/header.rb CHANGED
@@ -1,136 +1,89 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  require "akami"
4
3
  require "gyoku"
5
- require "savon/addressing"
4
+ require "securerandom"
6
5
 
7
6
  module Savon
8
- # Assembles the SOAP +Header+ element of a request envelope and renders it to
9
- # XML.
10
- #
11
- # A SOAP Header carries a request's out-of-band metadata. This object is
12
- # responsible for every header block Savon can emit, concatenated in this
13
- # order:
14
- #
15
- # 1. application headers supplied by the caller (the +soap_header+ option),
16
- # 2. the WS-Addressing headers, rendered by {Savon::Addressing},
17
- # 3. the WS-Security header with UsernameToken credentials, a +wsu:Timestamp+
18
- # and an XML Signature (OASIS WSS SOAP Message Security 1.1).
19
- #
20
- # WS-Security markup is delegated to Akami and Hash-to-XML conversion to Gyoku.
21
7
  class Header
22
- # @param globals [Savon::GlobalOptions] client-level options, read for the
23
- # Gyoku key converter (+:convert_request_keys_to+) and the WS-Addressing
24
- # toggle (+:use_wsa_headers+)
25
- # @param effective [Savon::EffectiveOptions] resolves the WSSE, soap_header,
26
- # soap_action and endpoint values for the request
27
- def initialize(globals, effective)
28
- @gyoku_options = { key_converter: globals[:convert_request_keys_to] }
29
-
30
- @wsse_auth = effective.wsse_auth
31
- @wsse_timestamp = effective.wsse_timestamp
32
- @wsse_signature = effective.wsse_signature
33
- @soap_header = effective.soap_header
34
-
35
- @addressing = build_addressing(globals, effective)
36
- @header = build
37
- end
38
8
 
39
- attr_reader :gyoku_options, :wsse_auth, :wsse_timestamp, :wsse_signature
9
+ def initialize(globals, locals)
10
+ @gyoku_options = { :key_converter => globals[:convert_request_keys_to] }
11
+
12
+ @wsse_auth = locals[:wsse_auth].nil? ? globals[:wsse_auth] : locals[:wsse_auth]
13
+ @wsse_timestamp = locals[:wsse_timestamp].nil? ? globals[:wsse_timestamp] : locals[:wsse_timestamp]
14
+ @wsse_signature = locals[:wsse_signature].nil? ? globals[:wsse_signature] : locals[:wsse_signature]
40
15
 
41
- # Returns the XML attributes of the enclosing Header element. Only the
42
- # WS-Addressing section declares one, the +xmlns:wsa+ prefix.
43
- #
44
- # @return [Hash{String => String}]
45
- def attributes
46
- @addressing.namespace_attributes
16
+ @global_header = globals[:soap_header]
17
+ @local_header = locals[:soap_header]
18
+
19
+ @globals = globals
20
+ @locals = locals
21
+
22
+ @header = build
47
23
  end
48
24
 
49
- # @return [Boolean] whether the rendered header is empty, i.e. there is no
50
- # header content to include in the envelope
25
+ attr_reader :local_header, :global_header, :gyoku_options,
26
+ :wsse_auth, :wsse_timestamp, :wsse_signature
27
+
51
28
  def empty?
52
29
  @header.empty?
53
30
  end
54
31
 
55
- # Returns the rendered SOAP header XML. The header is built once during
56
- # construction and cached, so this is a cheap, repeatable read.
57
- #
58
- # @return [String] the header XML (may be empty)
59
32
  def to_s
60
33
  @header
61
34
  end
62
35
 
63
36
  private
64
37
 
65
- # Constructs the WS-Addressing section from the resolved request values.
66
- # Resolving the endpoint raises without a WSDL document and without a
67
- # global +:endpoint+, so action and endpoint are only resolved when the
68
- # +:use_wsa_headers+ global asks for the headers.
69
- #
70
- # @return [Savon::Addressing]
71
- def build_addressing(globals, effective)
72
- return Addressing.new(enabled: false) unless globals[:use_wsa_headers]
73
-
74
- Addressing.new(
75
- enabled: true,
76
- action: effective.soap_action,
77
- to: effective.endpoint
78
- )
79
- end
80
-
81
- # Concatenates the three header sections in document order: caller content,
82
- # WS-Addressing, then WSSE security.
83
- #
84
- # @return [String]
85
38
  def build
86
- build_header + @addressing.to_xml + build_wsse_header
39
+ build_header + build_wsa_header + build_wsse_header
87
40
  end
88
41
 
89
- # Renders the resolved soap_header. {Savon::EffectiveOptions#soap_header}
90
- # merges the global and local values, so there is nothing to combine here.
91
- # Hash-to-XML conversion only. Strings pass through.
92
- #
93
- # @return [String]
94
42
  def build_header
95
- convert_to_xml(@soap_header)
43
+ header =
44
+ if global_header.kind_of?(Hash) && local_header.kind_of?(Hash)
45
+ global_header.merge(local_header)
46
+ elsif local_header
47
+ local_header
48
+ else
49
+ global_header
50
+ end
51
+
52
+ convert_to_xml(header)
96
53
  end
97
54
 
98
- # Builds the WS-Security header via Akami, or an empty string when no WSSE
99
- # content (credentials, timestamp or signature) applies.
100
- #
101
- # @return [String]
102
55
  def build_wsse_header
103
56
  wsse_header = akami
104
57
  wsse_header.respond_to?(:to_xml) ? wsse_header.to_xml : ""
105
58
  end
106
59
 
107
- # Renders a header value to XML. A Hash goes through Gyoku (honouring the
108
- # configured key converter), anything else is coerced with +#to_s+.
109
- #
110
- # @param hash_or_string [Hash, #to_s] the header content
111
- # @return [String]
60
+ def build_wsa_header
61
+ return '' unless @globals[:use_wsa_headers]
62
+ convert_to_xml({
63
+ 'wsa:Action' => @locals[:soap_action],
64
+ 'wsa:To' => @globals[:endpoint],
65
+ 'wsa:MessageID' => "urn:uuid:#{SecureRandom.uuid}"
66
+ })
67
+ end
68
+
112
69
  def convert_to_xml(hash_or_string)
113
- if hash_or_string.is_a? Hash
70
+ if hash_or_string.kind_of? Hash
114
71
  Gyoku.xml(hash_or_string, gyoku_options)
115
72
  else
116
73
  hash_or_string.to_s
117
74
  end
118
75
  end
119
76
 
120
- # Configures an Akami WSSE object from the resolved WSSE options. Sets the
121
- # UsernameToken credentials, enables the wsu:Timestamp, and attaches the XML
122
- # Signature when a document has been assigned to it.
123
- #
124
- # @return [Akami::WSSE] configured, not yet serialized
125
77
  def akami
126
78
  wsse = Akami.wsse
127
79
  wsse.credentials(*wsse_auth) if wsse_auth
128
80
  wsse.timestamp = wsse_timestamp if wsse_timestamp
129
- if wsse_signature&.have_document?
81
+ if wsse_signature && wsse_signature.have_document?
130
82
  wsse.signature = wsse_signature
131
83
  end
132
84
 
133
85
  wsse
134
86
  end
87
+
135
88
  end
136
89
  end
@@ -2,26 +2,26 @@
2
2
 
3
3
  module Savon
4
4
  class HTTPError < Error
5
+
5
6
  def self.present?(http)
6
- http.error?
7
+ !http.success?
7
8
  end
8
9
 
9
- def initialize(transport_response)
10
- @transport_response = transport_response
10
+ def initialize(http)
11
+ @http = http
11
12
  end
12
13
 
13
- def http
14
- @transport_response
15
- end
14
+ attr_reader :http
16
15
 
17
16
  def to_s
18
- String.new("HTTP error (#{http.code})").tap do |str_error|
19
- str_error << ": #{http.body}" unless http.body.empty?
17
+ String.new("HTTP error (#{@http.status})").tap do |str_error|
18
+ str_error << ": #{@http.body}" unless @http.body.empty?
20
19
  end
21
20
  end
22
21
 
23
22
  def to_hash
24
- { code: http.code, headers: http.headers, body: http.body }
23
+ { :code => @http.status, :headers => @http.headers, :body => @http.body }
25
24
  end
25
+
26
26
  end
27
27
  end
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  require "nokogiri"
4
3
 
5
4
  module Savon
6
5
  class LogMessage
6
+
7
7
  def initialize(message, filters = [], pretty_print = false)
8
8
  @message = message
9
9
  @filters = filters
@@ -46,7 +46,8 @@ module Savon
46
46
  end
47
47
 
48
48
  def nokogiri_options
49
- @pretty_print ? { indent: 2 } : { save_with: Nokogiri::XML::Node::SaveOptions::AS_XML }
49
+ @pretty_print ? { :indent => 2 } : { :save_with => Nokogiri::XML::Node::SaveOptions::AS_XML }
50
50
  end
51
+
51
52
  end
52
53
  end
data/lib/savon/message.rb CHANGED
@@ -1,10 +1,10 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  require "savon/qualified_message"
4
3
  require "gyoku"
5
4
 
6
5
  module Savon
7
6
  class Message
7
+
8
8
  def initialize(message_tag, namespace_identifier, types, used_namespaces, message, element_form_default, key_converter, unwrap)
9
9
  @message_tag = message_tag
10
10
  @namespace_identifier = namespace_identifier
@@ -18,20 +18,21 @@ module Savon
18
18
  end
19
19
 
20
20
  def to_s
21
- return @message.to_s unless @message.is_a? Hash
21
+ return @message.to_s unless @message.kind_of? Hash
22
22
 
23
23
  if @element_form_default == :qualified
24
24
  @message = QualifiedMessage.new(@types, @used_namespaces, @key_converter).to_hash(@message, [@message_tag.to_s])
25
25
  end
26
26
 
27
27
  gyoku_options = {
28
- element_form_default: @element_form_default,
29
- namespace: @namespace_identifier,
30
- key_converter: @key_converter,
31
- unwrap: @unwrap
28
+ :element_form_default => @element_form_default,
29
+ :namespace => @namespace_identifier,
30
+ :key_converter => @key_converter,
31
+ :unwrap => @unwrap
32
32
  }
33
33
 
34
34
  Gyoku.xml(@message, gyoku_options)
35
35
  end
36
+
36
37
  end
37
38
  end
@@ -1,18 +1,11 @@
1
1
  # frozen_string_literal: true
2
-
3
- require "httpi"
4
- require "savon/transport/response"
2
+ require "faraday"
5
3
 
6
4
  module Savon
7
- # A single test expectation set up by Savon's mock interface.
8
- # One expectation covers one operation call in one test.
9
- #
10
- # Records the expected operation name and message, captures what was
11
- # actually called, and either returns a synthetic response or raises
12
- # an error on mismatch.
13
5
  class MockExpectation
6
+
14
7
  def initialize(operation_name)
15
- @expected = { operation_name: operation_name }
8
+ @expected = { :operation_name => operation_name }
16
9
  @actual = nil
17
10
  end
18
11
 
@@ -22,15 +15,15 @@ module Savon
22
15
  end
23
16
 
24
17
  def returns(response)
25
- response = { code: 200, headers: {}, body: response } if response.is_a?(String)
18
+ response = { :code => 200, :headers => {}, :body => response } if response.kind_of?(String)
26
19
  @response = response
27
20
  self
28
21
  end
29
22
 
30
- def actual(operation_name, _builder, _globals, locals)
23
+ def actual(operation_name, builder, globals, locals)
31
24
  @actual = {
32
- operation_name: operation_name,
33
- message: locals[:message]
25
+ :operation_name => operation_name,
26
+ :message => locals[:message]
34
27
  }
35
28
  end
36
29
 
@@ -44,49 +37,42 @@ module Savon
44
37
  verify_message!
45
38
  end
46
39
 
47
- # Builds and returns a Transport::Response from the configured response hash.
48
- #
49
- # @return [Transport::Response]
50
- # @raise [ExpectationError] if no response was configured for this expectation
51
40
  def response!
52
41
  unless @response
53
42
  raise ExpectationError, "This expectation was not set up with a response."
54
43
  end
55
-
56
- Transport::Response.new(@response[:code], @response[:headers], @response[:body])
44
+ env = Faraday::Env.from(status: @response[:code], response_headers: @response[:headers], response_body: @response[:body])
45
+ Faraday::Response.new(env)
57
46
  end
58
47
 
59
48
  private
60
49
 
61
50
  def verify_operation_name!
62
- return if @expected[:operation_name] == @actual[:operation_name]
63
-
64
- raise ExpectationError, "Expected a request to the #{@expected[:operation_name].inspect} operation.\n" \
65
- "Received a request to the #{@actual[:operation_name].inspect} operation instead."
51
+ unless @expected[:operation_name] == @actual[:operation_name]
52
+ raise ExpectationError, "Expected a request to the #{@expected[:operation_name].inspect} operation.\n" \
53
+ "Received a request to the #{@actual[:operation_name].inspect} operation instead."
54
+ end
66
55
  end
67
56
 
68
57
  def verify_message!
69
58
  return if @expected[:message].eql? :any
59
+ unless equals_except_any(@expected[:message], @actual[:message])
60
+ expected_message = " with this message: #{@expected[:message].inspect}" if @expected[:message]
61
+ expected_message ||= " with no message."
70
62
 
71
- return if equals_except_any(@expected[:message], @actual[:message])
63
+ actual_message = " with this message: #{@actual[:message].inspect}" if @actual[:message]
64
+ actual_message ||= " with no message."
72
65
 
73
- expected_message = " with this message: #{@expected[:message].inspect}" if @expected[:message]
74
- expected_message ||= " with no message."
75
-
76
- actual_message = " with this message: #{@actual[:message].inspect}" if @actual[:message]
77
- actual_message ||= " with no message."
78
-
79
- raise ExpectationError, "Expected a request to the #{@expected[:operation_name].inspect} operation\n#{expected_message}\n" \
80
- "Received a request to the #{@actual[:operation_name].inspect} operation\n#{actual_message}"
66
+ raise ExpectationError, "Expected a request to the #{@expected[:operation_name].inspect} operation\n#{expected_message}\n" \
67
+ "Received a request to the #{@actual[:operation_name].inspect} operation\n#{actual_message}"
68
+ end
81
69
  end
82
70
 
83
71
  def equals_except_any(msg_expected, msg_real)
84
- # === allows RSpec matchers (e.g. include(:key)) to be used as expected values
85
- return true if msg_expected === msg_real # rubocop:disable Style/CaseEquality
86
- return false if msg_expected.nil? || msg_real.nil? # If both are nil has returned true
87
-
72
+ return true if msg_expected === msg_real
73
+ return false if (msg_expected.nil? || msg_real.nil?) # If both are nil has returned true
88
74
  msg_expected.each do |key, expected_value|
89
- next if expected_value == :any && msg_real.include?(key)
75
+ next if (expected_value == :any && msg_real.include?(key))
90
76
  return false if expected_value != msg_real[key]
91
77
  end
92
78
  true
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  require "savon/mock"
4
3
 
5
4
  module Savon
6
5
  module SpecHelper
6
+
7
7
  class Interface
8
+
8
9
  def mock!
9
10
  Savon.observers << self
10
11
  end
@@ -26,12 +27,14 @@ module Savon
26
27
  def notify(operation_name, builder, globals, locals)
27
28
  expectation = expectations.shift
28
29
 
29
- raise ExpectationError, "Unexpected request to the #{operation_name.inspect} operation." unless expectation
30
-
31
- expectation.actual(operation_name, builder, globals, locals)
30
+ if expectation
31
+ expectation.actual(operation_name, builder, globals, locals)
32
32
 
33
- expectation.verify!
34
- expectation.response!
33
+ expectation.verify!
34
+ expectation.response!
35
+ else
36
+ raise ExpectationError, "Unexpected request to the #{operation_name.inspect} operation."
37
+ end
35
38
  rescue ExpectationError
36
39
  @expectations.clear
37
40
  raise
@@ -39,12 +42,12 @@ module Savon
39
42
 
40
43
  def verify!
41
44
  return if expectations.empty?
42
-
43
45
  expectations.each(&:verify!)
44
46
  rescue ExpectationError
45
47
  @expectations.clear
46
48
  raise
47
49
  end
50
+
48
51
  end
49
52
 
50
53
  def savon
@@ -55,5 +58,6 @@ module Savon
55
58
  super if defined? super
56
59
  savon.verify!
57
60
  end
61
+
58
62
  end
59
63
  end
data/lib/savon/mock.rb CHANGED
@@ -1,5 +1,4 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  module Savon
4
3
  class ExpectationError < StandardError; end
5
4
  end
data/lib/savon/model.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
-
3
2
  module Savon
4
3
  module Model
4
+
5
5
  def self.extended(base)
6
6
  base.setup
7
7
  end
@@ -28,30 +28,26 @@ module Savon
28
28
 
29
29
  # Defines a class-level SOAP operation.
30
30
  def define_class_operation(operation)
31
- method_name = operation_method_name(operation)
32
-
33
- class_operation_module.define_method(method_name) do |locals = {}|
34
- client.call operation, locals
35
- end
31
+ class_operation_module.module_eval %{
32
+ def #{StringUtils.snakecase(operation.to_s)}(locals = {})
33
+ client.call #{operation.inspect}, locals
34
+ end
35
+ }
36
36
  end
37
37
 
38
38
  # Defines an instance-level SOAP operation.
39
39
  def define_instance_operation(operation)
40
- method_name = operation_method_name(operation)
41
-
42
- instance_operation_module.define_method(method_name) do |locals = {}|
43
- self.class.public_send(method_name, locals)
44
- end
45
- end
46
-
47
- # Returns the generated Ruby method name for a SOAP operation.
48
- def operation_method_name(operation)
49
- StringUtils.snakecase(operation.to_s).to_sym
40
+ instance_operation_module.module_eval %{
41
+ def #{StringUtils.snakecase(operation.to_s)}(locals = {})
42
+ self.class.#{StringUtils.snakecase(operation.to_s)} locals
43
+ end
44
+ }
50
45
  end
51
46
 
52
47
  # Class methods.
53
48
  def class_operation_module
54
- @class_operation_module ||= Module.new do
49
+ @class_operation_module ||= Module.new {
50
+
55
51
  def client(globals = {})
56
52
  @client ||= Savon::Client.new(globals)
57
53
  rescue InitializationError
@@ -64,22 +60,26 @@ module Savon
64
60
 
65
61
  def raise_initialization_error!
66
62
  raise InitializationError,
67
- "Expected the model to be initialized with either a WSDL document or the SOAP endpoint and target namespace options.\n" \
68
- "Make sure to setup the model by calling the .client class method before calling the .global method.\n\n" \
69
- "client(wsdl: '/Users/me/project/service.wsdl') # to use a local WSDL document\n" \
70
- "client(wsdl: 'http://example.com?wsdl') # to use a remote WSDL document\n" \
71
- "client(endpoint: 'http://example.com', namespace: 'http://v1.example.com') # if you don't have a WSDL document"
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"
72
68
  end
73
- end.tap { |mod| extend(mod) }
69
+
70
+ }.tap { |mod| extend(mod) }
74
71
  end
75
72
 
76
73
  # Instance methods.
77
74
  def instance_operation_module
78
- @instance_operation_module ||= Module.new do
75
+ @instance_operation_module ||= Module.new {
76
+
79
77
  def client
80
78
  self.class.client
81
79
  end
82
- end.tap { |mod| include(mod) }
80
+
81
+ }.tap { |mod| include(mod) }
83
82
  end
83
+
84
84
  end
85
85
  end