active_rest_client 0.9.58
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +17 -0
- data/.rspec +2 -0
- data/.simplecov +4 -0
- data/Gemfile +4 -0
- data/Guardfile +9 -0
- data/LICENSE.txt +22 -0
- data/README.md +585 -0
- data/Rakefile +3 -0
- data/active_rest_client.gemspec +34 -0
- data/lib/active_rest_client.rb +23 -0
- data/lib/active_rest_client/base.rb +128 -0
- data/lib/active_rest_client/caching.rb +84 -0
- data/lib/active_rest_client/configuration.rb +69 -0
- data/lib/active_rest_client/connection.rb +76 -0
- data/lib/active_rest_client/connection_manager.rb +21 -0
- data/lib/active_rest_client/headers_list.rb +47 -0
- data/lib/active_rest_client/instrumentation.rb +62 -0
- data/lib/active_rest_client/lazy_association_loader.rb +95 -0
- data/lib/active_rest_client/lazy_loader.rb +23 -0
- data/lib/active_rest_client/logger.rb +67 -0
- data/lib/active_rest_client/mapping.rb +65 -0
- data/lib/active_rest_client/proxy_base.rb +143 -0
- data/lib/active_rest_client/recording.rb +24 -0
- data/lib/active_rest_client/request.rb +412 -0
- data/lib/active_rest_client/request_filtering.rb +52 -0
- data/lib/active_rest_client/result_iterator.rb +66 -0
- data/lib/active_rest_client/validation.rb +60 -0
- data/lib/active_rest_client/version.rb +3 -0
- data/spec/lib/base_spec.rb +245 -0
- data/spec/lib/caching_spec.rb +179 -0
- data/spec/lib/configuration_spec.rb +105 -0
- data/spec/lib/connection_manager_spec.rb +36 -0
- data/spec/lib/connection_spec.rb +73 -0
- data/spec/lib/headers_list_spec.rb +61 -0
- data/spec/lib/instrumentation_spec.rb +59 -0
- data/spec/lib/lazy_association_loader_spec.rb +118 -0
- data/spec/lib/lazy_loader_spec.rb +25 -0
- data/spec/lib/logger_spec.rb +63 -0
- data/spec/lib/mapping_spec.rb +48 -0
- data/spec/lib/proxy_spec.rb +154 -0
- data/spec/lib/recording_spec.rb +34 -0
- data/spec/lib/request_filtering_spec.rb +72 -0
- data/spec/lib/request_spec.rb +471 -0
- data/spec/lib/result_iterator_spec.rb +104 -0
- data/spec/lib/validation_spec.rb +113 -0
- data/spec/spec_helper.rb +22 -0
- metadata +265 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
module ActiveRestClient
|
2
|
+
module Recording
|
3
|
+
module ClassMethods
|
4
|
+
@record_response = nil
|
5
|
+
|
6
|
+
def record_response(url = nil, response = nil, &block)
|
7
|
+
if url && response && @record_response
|
8
|
+
@record_response.call(url, response)
|
9
|
+
elsif block
|
10
|
+
@record_response = block
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def record_response?
|
15
|
+
!!@record_response
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.included(base)
|
20
|
+
base.extend(ClassMethods)
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,412 @@
|
|
1
|
+
require "cgi"
|
2
|
+
require "oj"
|
3
|
+
|
4
|
+
module ActiveRestClient
|
5
|
+
|
6
|
+
class Request
|
7
|
+
attr_accessor :post_params, :get_params, :url, :path, :headers, :method, :object, :body, :forced_url, :original_url
|
8
|
+
|
9
|
+
def initialize(method, object, params = {})
|
10
|
+
@method = method
|
11
|
+
@method[:options] ||= {}
|
12
|
+
@method[:options][:lazy] ||= []
|
13
|
+
@overriden_name = @method[:options][:overriden_name]
|
14
|
+
@object = object
|
15
|
+
@params = params
|
16
|
+
@headers = HeadersList.new
|
17
|
+
end
|
18
|
+
|
19
|
+
def object_is_class?
|
20
|
+
!@object.respond_to?(:dirty?)
|
21
|
+
end
|
22
|
+
|
23
|
+
def class_name
|
24
|
+
if object_is_class?
|
25
|
+
@object.name
|
26
|
+
else
|
27
|
+
@object.class.name
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def base_url
|
32
|
+
if object_is_class?
|
33
|
+
@object.base_url
|
34
|
+
else
|
35
|
+
@object.class.base_url
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def verbose?
|
40
|
+
if object_is_class?
|
41
|
+
@object.verbose
|
42
|
+
else
|
43
|
+
@object.class.verbose
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def translator
|
48
|
+
if object_is_class?
|
49
|
+
@object.translator
|
50
|
+
else
|
51
|
+
@object.class.translator
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def proxy
|
56
|
+
if object_is_class?
|
57
|
+
@object.proxy
|
58
|
+
else
|
59
|
+
@object.class.proxy
|
60
|
+
end
|
61
|
+
rescue
|
62
|
+
nil
|
63
|
+
end
|
64
|
+
|
65
|
+
def http_method
|
66
|
+
@method[:method]
|
67
|
+
end
|
68
|
+
|
69
|
+
def call(explicit_parameters=nil)
|
70
|
+
@instrumentation_name = "#{class_name}##{@method[:name]}"
|
71
|
+
result = nil
|
72
|
+
cached = nil
|
73
|
+
ActiveSupport::Notifications.instrument("request_call.active_rest_client", :name => @instrumentation_name) do
|
74
|
+
if @method[:options][:fake]
|
75
|
+
ActiveRestClient::Logger.debug " \033[1;4;32m#{ActiveRestClient::NAME}\033[0m #{@instrumentation_name} - Faked response found"
|
76
|
+
return handle_response(OpenStruct.new(status:200, body:@method[:options][:fake], headers:{"X-ARC-Faked-Response" => "true"}))
|
77
|
+
end
|
78
|
+
@explicit_parameters = explicit_parameters
|
79
|
+
@body = nil
|
80
|
+
prepare_params
|
81
|
+
prepare_url
|
82
|
+
if object_is_class?
|
83
|
+
@object.send(:_filter_request, @method[:name], self)
|
84
|
+
else
|
85
|
+
@object.class.send(:_filter_request, @method[:name], self)
|
86
|
+
end
|
87
|
+
append_get_parameters
|
88
|
+
prepare_request_body
|
89
|
+
self.original_url = self.url
|
90
|
+
cached = ActiveRestClient::Base.read_cached_response(self)
|
91
|
+
if cached
|
92
|
+
if cached.expires && cached.expires > Time.now
|
93
|
+
ActiveRestClient::Logger.debug " \033[1;4;32m#{ActiveRestClient::NAME}\033[0m #{@instrumentation_name} - Absolutely cached copy found"
|
94
|
+
return handle_cached_response(cached)
|
95
|
+
elsif cached.etag.present?
|
96
|
+
ActiveRestClient::Logger.debug " \033[1;4;32m#{ActiveRestClient::NAME}\033[0m #{@instrumentation_name} - Etag cached copy found with etag #{cached.etag}"
|
97
|
+
etag = cached.etag
|
98
|
+
end
|
99
|
+
end
|
100
|
+
response = if proxy
|
101
|
+
proxy.handle(self) do |request|
|
102
|
+
request.do_request(etag)
|
103
|
+
end
|
104
|
+
else
|
105
|
+
do_request(etag)
|
106
|
+
end
|
107
|
+
if object_is_class? && @object.record_response?
|
108
|
+
@object.record_response(self.url, response)
|
109
|
+
end
|
110
|
+
result = handle_response(response)
|
111
|
+
if result == :not_modified && cached
|
112
|
+
result = cached.result
|
113
|
+
end
|
114
|
+
ActiveRestClient::Base.write_cached_response(self, response, result)
|
115
|
+
result
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
def prepare_params
|
120
|
+
params = @params || @object._attributes rescue {}
|
121
|
+
if params.is_a?(String) || params.is_a?(Fixnum)
|
122
|
+
params = {id:params}
|
123
|
+
end
|
124
|
+
|
125
|
+
default_params = @method[:options][:defaults] || {}
|
126
|
+
|
127
|
+
if @explicit_parameters
|
128
|
+
params = @explicit_parameters
|
129
|
+
end
|
130
|
+
if http_method == :get
|
131
|
+
@get_params = default_params.merge(params || {})
|
132
|
+
@post_params = nil
|
133
|
+
else
|
134
|
+
@post_params = default_params.merge(params || {})
|
135
|
+
@get_params = {}
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def prepare_url
|
140
|
+
if @forced_url && @forced_url.present?
|
141
|
+
@url = @forced_url
|
142
|
+
else
|
143
|
+
@url = @method[:url].dup
|
144
|
+
matches = @url.scan(/(:[a-z_-]+)/)
|
145
|
+
@get_params ||= {}
|
146
|
+
@post_params ||= {}
|
147
|
+
matches.each do |token|
|
148
|
+
token = token.first[1,999]
|
149
|
+
target = @get_params.delete(token.to_sym) || @post_params.delete(token.to_sym) || @get_params.delete(token.to_s) || @post_params.delete(token.to_s) || ""
|
150
|
+
@url.gsub!(":#{token}", target.to_s)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def append_get_parameters
|
156
|
+
if @get_params.any?
|
157
|
+
params = @get_params.map {|k,v| "#{k}=#{CGI.escape(v.to_s)}"}
|
158
|
+
@url += "?" + params.sort * "&"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def prepare_request_body(params = nil)
|
163
|
+
@body ||= (params || @post_params || {}).map {|k,v| "#{k}=#{CGI.escape(v.to_s)}"}.sort * "&"
|
164
|
+
end
|
165
|
+
|
166
|
+
def do_request(etag)
|
167
|
+
http_headers = {}
|
168
|
+
http_headers["If-None-Match"] = etag if etag
|
169
|
+
http_headers["Accept"] = "application/hal+json, application/json;q=0.5"
|
170
|
+
headers.each do |key,value|
|
171
|
+
value = value.join(",") if value.is_a?(Array)
|
172
|
+
http_headers[key] = value
|
173
|
+
end
|
174
|
+
if @method[:options][:url] || @forced_url
|
175
|
+
@url = @method[:options][:url]
|
176
|
+
@url = @forced_url if @forced_url
|
177
|
+
if connection = ActiveRestClient::ConnectionManager.find_connection_for_url(@url)
|
178
|
+
@url = @url.slice(connection.base_url.length, 255)
|
179
|
+
else
|
180
|
+
parts = @url.match(%r{^(https?://[a-z\d\.:-]+?)(/.*)}).to_a
|
181
|
+
if (parts.empty?) # Not a full URL, so use hostname/protocol from existing base_url
|
182
|
+
uri = URI.parse(base_url)
|
183
|
+
@base_url = "#{uri.scheme}://#{uri.host}#{":#{uri.port}" if uri.port != 80 && uri.port != 443}"
|
184
|
+
else
|
185
|
+
_, @base_url, @url = parts
|
186
|
+
end
|
187
|
+
connection = ActiveRestClient::ConnectionManager.get_connection(@base_url)
|
188
|
+
end
|
189
|
+
else
|
190
|
+
connection = ActiveRestClient::ConnectionManager.get_connection(base_url)
|
191
|
+
end
|
192
|
+
ActiveRestClient::Logger.info " \033[1;4;32m#{ActiveRestClient::NAME}\033[0m #{@instrumentation_name} - Requesting #{connection.base_url}#{@url}"
|
193
|
+
|
194
|
+
if verbose?
|
195
|
+
ActiveRestClient::Logger.debug "ActiveRestClient Verbose Log:"
|
196
|
+
ActiveRestClient::Logger.debug " > GET #{@url} HTTP/1.1"
|
197
|
+
http_headers.each do |k,v|
|
198
|
+
ActiveRestClient::Logger.debug " > #{k} : #{v}"
|
199
|
+
end
|
200
|
+
ActiveRestClient::Logger.debug " > #{@body}"
|
201
|
+
end
|
202
|
+
|
203
|
+
case http_method
|
204
|
+
when :get
|
205
|
+
response = connection.get(@url, http_headers)
|
206
|
+
when :put
|
207
|
+
response = connection.put(@url, @body, http_headers)
|
208
|
+
when :post
|
209
|
+
response = connection.post(@url, @body, http_headers)
|
210
|
+
when :delete
|
211
|
+
response = connection.delete(@url, http_headers)
|
212
|
+
else
|
213
|
+
raise InvalidRequestException.new("Invalid method #{http_method}")
|
214
|
+
end
|
215
|
+
|
216
|
+
if verbose?
|
217
|
+
response.headers.each do |k,v|
|
218
|
+
ActiveRestClient::Logger.debug " < #{k} : #{v}"
|
219
|
+
end
|
220
|
+
ActiveRestClient::Logger.debug " < #{response.body}"
|
221
|
+
end
|
222
|
+
|
223
|
+
response
|
224
|
+
end
|
225
|
+
|
226
|
+
def handle_cached_response(cached)
|
227
|
+
if cached.result.is_a? ActiveRestClient::ResultIterator
|
228
|
+
cached.result
|
229
|
+
else
|
230
|
+
if object_is_class?
|
231
|
+
cached.result
|
232
|
+
else
|
233
|
+
@object._copy_from(cached.result)
|
234
|
+
@object
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
|
239
|
+
def handle_response(response)
|
240
|
+
if response.status == 304
|
241
|
+
ActiveRestClient::Logger.debug " \033[1;4;32m#{ActiveRestClient::NAME}\033[0m #{@instrumentation_name} - Etag copy is the same as the server"
|
242
|
+
return :not_modified
|
243
|
+
end
|
244
|
+
if response.respond_to?(:proxied) && response.proxied
|
245
|
+
ActiveRestClient::Logger.debug " \033[1;4;32m#{ActiveRestClient::NAME}\033[0m #{@instrumentation_name} - Response was proxied, unable to determine size"
|
246
|
+
else
|
247
|
+
ActiveRestClient::Logger.debug " \033[1;4;32m#{ActiveRestClient::NAME}\033[0m #{@instrumentation_name} - Response received #{response.body.size} bytes"
|
248
|
+
end
|
249
|
+
@response = response
|
250
|
+
|
251
|
+
if response.body.is_a?(Array) || response.body.is_a?(Hash)
|
252
|
+
body = response.body
|
253
|
+
else
|
254
|
+
body = Oj.load(response.body) || {}
|
255
|
+
end
|
256
|
+
body = begin
|
257
|
+
@method[:name].nil? ? body : translator.send(@method[:name], body)
|
258
|
+
rescue NoMethodError
|
259
|
+
body
|
260
|
+
end
|
261
|
+
if body.is_a? Array
|
262
|
+
result = ActiveRestClient::ResultIterator.new(response.status)
|
263
|
+
body.each do |json_object|
|
264
|
+
result << new_object(json_object, @overriden_name)
|
265
|
+
end
|
266
|
+
else
|
267
|
+
result = new_object(body, @overriden_name)
|
268
|
+
result._status = response.status
|
269
|
+
unless object_is_class?
|
270
|
+
@object._copy_from(result)
|
271
|
+
result = @object
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
response.status ||= 200
|
276
|
+
if response.status == 401
|
277
|
+
raise HTTPUnauthorisedClientException.new(status:response.status, result:result, url:@url)
|
278
|
+
elsif response.status == 403
|
279
|
+
raise HTTPForbiddenClientException.new(status:response.status, result:result, url:@url)
|
280
|
+
elsif response.status == 404
|
281
|
+
raise HTTPNotFoundClientException.new(status:response.status, result:result, url:@url)
|
282
|
+
elsif (400..499).include? response.status
|
283
|
+
raise HTTPClientException.new(status:response.status, result:result, url:@url)
|
284
|
+
elsif (500..599).include? response.status
|
285
|
+
raise HTTPServerException.new(status:response.status, result:result, url:@url)
|
286
|
+
end
|
287
|
+
|
288
|
+
result
|
289
|
+
rescue Oj::ParseError
|
290
|
+
raise ResponseParseException.new(status:response.status, body:response.body)
|
291
|
+
end
|
292
|
+
|
293
|
+
def new_object(attributes, name = nil)
|
294
|
+
@method[:options][:has_many] ||= {}
|
295
|
+
name = name.to_sym rescue nil
|
296
|
+
if @method[:options][:has_many][name]
|
297
|
+
overriden_name = name
|
298
|
+
object = @method[:options][:has_many][name].new
|
299
|
+
else
|
300
|
+
if object_is_class?
|
301
|
+
object = @object.new
|
302
|
+
else
|
303
|
+
object = @object.class.new
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
if hal_response? && name.nil?
|
308
|
+
attributes = handle_hal_links_embedded(object, attributes)
|
309
|
+
end
|
310
|
+
|
311
|
+
attributes.each do |k,v|
|
312
|
+
k = k.to_sym
|
313
|
+
if @method[:options][:lazy].include?(k)
|
314
|
+
object._attributes[k] = ActiveRestClient::LazyAssociationLoader.new(overriden_name || k, v, self, overriden_name:(overriden_name||k))
|
315
|
+
elsif v.is_a? Hash
|
316
|
+
object._attributes[k] = new_object(v, overriden_name || k)
|
317
|
+
elsif v.is_a? Array
|
318
|
+
object._attributes[k] = ActiveRestClient::ResultIterator.new
|
319
|
+
v.each do |item|
|
320
|
+
if item.is_a? Hash
|
321
|
+
object._attributes[k] << new_object(item, overriden_name || k)
|
322
|
+
else
|
323
|
+
object._attributes[k] << item
|
324
|
+
end
|
325
|
+
end
|
326
|
+
else
|
327
|
+
if v.to_s[/\d{4}\-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})/]
|
328
|
+
object._attributes[k] = DateTime.parse(v)
|
329
|
+
else
|
330
|
+
object._attributes[k] = v
|
331
|
+
end
|
332
|
+
end
|
333
|
+
end
|
334
|
+
object.clean! unless object_is_class?
|
335
|
+
|
336
|
+
object
|
337
|
+
end
|
338
|
+
|
339
|
+
def hal_response?
|
340
|
+
_, content_type = @response.headers.detect{|k,v| k.downcase == "content-type"}
|
341
|
+
faked_response = @response.headers.detect{|k,v| k.downcase == "x-arc-faked-response"}
|
342
|
+
if content_type && content_type.respond_to?(:each)
|
343
|
+
content_type.each do |ct|
|
344
|
+
return true if ct[%r{application\/hal\+json}i]
|
345
|
+
return true if ct[%r{application\/json}i]
|
346
|
+
end
|
347
|
+
faked_response
|
348
|
+
elsif content_type && (content_type[%r{application\/hal\+json}i] || content_type[%r{application\/json}i]) || faked_response
|
349
|
+
true
|
350
|
+
else
|
351
|
+
false
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
def handle_hal_links_embedded(object, attributes)
|
356
|
+
attributes["_links"] = attributes[:_links] if attributes[:_links]
|
357
|
+
attributes["_embedded"] = attributes[:_embedded] if attributes[:_embedded]
|
358
|
+
if attributes["_links"]
|
359
|
+
attributes["_links"].each do |key, value|
|
360
|
+
if value.is_a?(Array)
|
361
|
+
object._attributes[key.to_sym] ||= ActiveRestClient::ResultIterator.new
|
362
|
+
value.each do |element|
|
363
|
+
begin
|
364
|
+
embedded_version = attributes["_embedded"][key].detect{|embed| embed["_links"]["self"]["href"] == element["href"]}
|
365
|
+
object._attributes[key.to_sym] << new_object(embedded_version, key)
|
366
|
+
rescue NoMethodError
|
367
|
+
object._attributes[key.to_sym] << ActiveRestClient::LazyAssociationLoader.new(key, element, self)
|
368
|
+
end
|
369
|
+
end
|
370
|
+
else
|
371
|
+
begin
|
372
|
+
embedded_version = attributes["_embedded"][key]
|
373
|
+
object._attributes[key.to_sym] = new_object(embedded_version, key)
|
374
|
+
rescue NoMethodError
|
375
|
+
object._attributes[key.to_sym] = ActiveRestClient::LazyAssociationLoader.new(key, value, self)
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
379
|
+
attributes.delete("_links")
|
380
|
+
attributes.delete("_embedded")
|
381
|
+
end
|
382
|
+
|
383
|
+
attributes
|
384
|
+
end
|
385
|
+
end
|
386
|
+
|
387
|
+
class RequestException < StandardError ; end
|
388
|
+
|
389
|
+
class InvalidRequestException < RequestException ; end
|
390
|
+
class ResponseParseException < RequestException
|
391
|
+
attr_accessor :status, :body
|
392
|
+
def initialize(options)
|
393
|
+
@status = options[:status]
|
394
|
+
@body = options[:body]
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
class HTTPException < RequestException
|
399
|
+
attr_accessor :status, :result, :request_url
|
400
|
+
def initialize(options)
|
401
|
+
@status = options[:status]
|
402
|
+
@result = options[:result]
|
403
|
+
@request_url = options[:url]
|
404
|
+
end
|
405
|
+
end
|
406
|
+
class HTTPClientException < HTTPException ; end
|
407
|
+
class HTTPUnauthorisedClientException < HTTPClientException ; end
|
408
|
+
class HTTPForbiddenClientException < HTTPClientException ; end
|
409
|
+
class HTTPNotFoundClientException < HTTPClientException ; end
|
410
|
+
class HTTPServerException < HTTPException ; end
|
411
|
+
|
412
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module ActiveRestClient
|
2
|
+
module RequestFiltering
|
3
|
+
module ClassMethods
|
4
|
+
def before_request(method_name = nil, &block)
|
5
|
+
@filters ||= []
|
6
|
+
if block
|
7
|
+
@filters << block
|
8
|
+
elsif method_name
|
9
|
+
@filters << method_name
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def _filter_request(name, request)
|
14
|
+
_handle_super_class_filters(name, request)
|
15
|
+
@filters ||= []
|
16
|
+
@filters.each do |filter|
|
17
|
+
if filter.is_a? Symbol
|
18
|
+
if self.respond_to?(filter)
|
19
|
+
self.send(filter, name, request)
|
20
|
+
else
|
21
|
+
instance = self.new
|
22
|
+
instance.send(filter, name, request)
|
23
|
+
end
|
24
|
+
else
|
25
|
+
filter.call(name, request)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def _handle_super_class_filters(name, request)
|
31
|
+
@parents ||= []
|
32
|
+
@parents.each do |parent|
|
33
|
+
parent._filter_request(name, request)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def _parents
|
38
|
+
@parents ||= []
|
39
|
+
end
|
40
|
+
|
41
|
+
def inherited(subclass)
|
42
|
+
subclass._parents << self
|
43
|
+
super
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.included(base)
|
48
|
+
base.extend(ClassMethods)
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|