active_rest_client 0.9.58

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 (48) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +17 -0
  3. data/.rspec +2 -0
  4. data/.simplecov +4 -0
  5. data/Gemfile +4 -0
  6. data/Guardfile +9 -0
  7. data/LICENSE.txt +22 -0
  8. data/README.md +585 -0
  9. data/Rakefile +3 -0
  10. data/active_rest_client.gemspec +34 -0
  11. data/lib/active_rest_client.rb +23 -0
  12. data/lib/active_rest_client/base.rb +128 -0
  13. data/lib/active_rest_client/caching.rb +84 -0
  14. data/lib/active_rest_client/configuration.rb +69 -0
  15. data/lib/active_rest_client/connection.rb +76 -0
  16. data/lib/active_rest_client/connection_manager.rb +21 -0
  17. data/lib/active_rest_client/headers_list.rb +47 -0
  18. data/lib/active_rest_client/instrumentation.rb +62 -0
  19. data/lib/active_rest_client/lazy_association_loader.rb +95 -0
  20. data/lib/active_rest_client/lazy_loader.rb +23 -0
  21. data/lib/active_rest_client/logger.rb +67 -0
  22. data/lib/active_rest_client/mapping.rb +65 -0
  23. data/lib/active_rest_client/proxy_base.rb +143 -0
  24. data/lib/active_rest_client/recording.rb +24 -0
  25. data/lib/active_rest_client/request.rb +412 -0
  26. data/lib/active_rest_client/request_filtering.rb +52 -0
  27. data/lib/active_rest_client/result_iterator.rb +66 -0
  28. data/lib/active_rest_client/validation.rb +60 -0
  29. data/lib/active_rest_client/version.rb +3 -0
  30. data/spec/lib/base_spec.rb +245 -0
  31. data/spec/lib/caching_spec.rb +179 -0
  32. data/spec/lib/configuration_spec.rb +105 -0
  33. data/spec/lib/connection_manager_spec.rb +36 -0
  34. data/spec/lib/connection_spec.rb +73 -0
  35. data/spec/lib/headers_list_spec.rb +61 -0
  36. data/spec/lib/instrumentation_spec.rb +59 -0
  37. data/spec/lib/lazy_association_loader_spec.rb +118 -0
  38. data/spec/lib/lazy_loader_spec.rb +25 -0
  39. data/spec/lib/logger_spec.rb +63 -0
  40. data/spec/lib/mapping_spec.rb +48 -0
  41. data/spec/lib/proxy_spec.rb +154 -0
  42. data/spec/lib/recording_spec.rb +34 -0
  43. data/spec/lib/request_filtering_spec.rb +72 -0
  44. data/spec/lib/request_spec.rb +471 -0
  45. data/spec/lib/result_iterator_spec.rb +104 -0
  46. data/spec/lib/validation_spec.rb +113 -0
  47. data/spec/spec_helper.rb +22 -0
  48. 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