active_rest_client 0.9.58

Sign up to get free protection for your applications and to get access to all the features.
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