flexirest 1.2.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.
Files changed (56) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +16 -0
  3. data/.rspec +2 -0
  4. data/.simplecov +4 -0
  5. data/.travis.yml +6 -0
  6. data/CHANGELOG.md +37 -0
  7. data/CONTRIBUTING.md +62 -0
  8. data/Gemfile +4 -0
  9. data/Guardfile +9 -0
  10. data/LICENSE.txt +22 -0
  11. data/README.md +846 -0
  12. data/Rakefile +13 -0
  13. data/doc/ActiveRestClient Internals.graffle +1236 -0
  14. data/doc/ActiveRestClient Internals.png +0 -0
  15. data/flexirest.gemspec +39 -0
  16. data/lib/flexirest.rb +25 -0
  17. data/lib/flexirest/base.rb +189 -0
  18. data/lib/flexirest/caching.rb +92 -0
  19. data/lib/flexirest/configuration.rb +209 -0
  20. data/lib/flexirest/connection.rb +103 -0
  21. data/lib/flexirest/connection_manager.rb +36 -0
  22. data/lib/flexirest/headers_list.rb +47 -0
  23. data/lib/flexirest/instrumentation.rb +62 -0
  24. data/lib/flexirest/lazy_association_loader.rb +97 -0
  25. data/lib/flexirest/lazy_loader.rb +23 -0
  26. data/lib/flexirest/logger.rb +67 -0
  27. data/lib/flexirest/mapping.rb +69 -0
  28. data/lib/flexirest/monkey_patching.rb +7 -0
  29. data/lib/flexirest/proxy_base.rb +193 -0
  30. data/lib/flexirest/recording.rb +24 -0
  31. data/lib/flexirest/request.rb +573 -0
  32. data/lib/flexirest/request_delegator.rb +44 -0
  33. data/lib/flexirest/request_filtering.rb +62 -0
  34. data/lib/flexirest/result_iterator.rb +85 -0
  35. data/lib/flexirest/validation.rb +60 -0
  36. data/lib/flexirest/version.rb +3 -0
  37. data/spec/lib/base_spec.rb +389 -0
  38. data/spec/lib/caching_spec.rb +217 -0
  39. data/spec/lib/configuration_spec.rb +234 -0
  40. data/spec/lib/connection_manager_spec.rb +43 -0
  41. data/spec/lib/connection_spec.rb +159 -0
  42. data/spec/lib/headers_list_spec.rb +61 -0
  43. data/spec/lib/instrumentation_spec.rb +58 -0
  44. data/spec/lib/lazy_association_loader_spec.rb +135 -0
  45. data/spec/lib/lazy_loader_spec.rb +25 -0
  46. data/spec/lib/logger_spec.rb +63 -0
  47. data/spec/lib/mapping_spec.rb +52 -0
  48. data/spec/lib/proxy_spec.rb +189 -0
  49. data/spec/lib/recording_spec.rb +34 -0
  50. data/spec/lib/request_filtering_spec.rb +84 -0
  51. data/spec/lib/request_spec.rb +711 -0
  52. data/spec/lib/result_iterator_spec.rb +140 -0
  53. data/spec/lib/validation_spec.rb +113 -0
  54. data/spec/lib/xml_spec.rb +74 -0
  55. data/spec/spec_helper.rb +88 -0
  56. metadata +347 -0
@@ -0,0 +1,7 @@
1
+ require 'faraday'
2
+
3
+ if defined?("Faraday::Env")
4
+ class Faraday::Env
5
+ alias_method :headers, :response_headers
6
+ end
7
+ end
@@ -0,0 +1,193 @@
1
+ require "uri"
2
+
3
+ module Flexirest
4
+ class ProxyBase
5
+ cattr_accessor :mappings, :request, :original_handler
6
+ cattr_accessor :original_body, :original_get_params, :original_post_params, :original_url
7
+
8
+ module ClassMethods
9
+ def get(match, &block)
10
+ add_mapping(:get, match, block)
11
+ end
12
+
13
+ def post(match, &block)
14
+ add_mapping(:post, match, block)
15
+ end
16
+
17
+ def put(match, &block)
18
+ add_mapping(:put, match, block)
19
+ end
20
+
21
+ def delete(match, &block)
22
+ add_mapping(:delete, match, block)
23
+ end
24
+
25
+ def add_mapping(method_type, match, block)
26
+ @mappings ||= []
27
+
28
+ if match.is_a?(String) && (param_keys = match.scan(/:\w+/)) && param_keys.any?
29
+ param_keys.each do |key|
30
+ match.gsub!(key, "([^/]+)")
31
+ end
32
+ param_keys = param_keys.map {|k| k.gsub(":", "").to_sym}
33
+ match = Regexp.new(match)
34
+ end
35
+
36
+ @mappings << OpenStruct.new(http_method:method_type, match:match, block:block, param_keys:param_keys)
37
+ end
38
+
39
+ def body(value = nil)
40
+ @body = value if value
41
+ @body
42
+ end
43
+
44
+ def url(value = nil)
45
+ @url = value if value
46
+ @url
47
+ end
48
+
49
+ def get_params(value = nil)
50
+ @get_params = value if value
51
+ @get_params
52
+ end
53
+
54
+ def post_params(value = nil)
55
+ @post_params = value if value
56
+ @post_params
57
+ end
58
+
59
+ def params(value = nil)
60
+ @params = value if value
61
+ @params
62
+ end
63
+
64
+ def passthrough
65
+ rebuild_request
66
+ @original_handler.call(@request)
67
+ end
68
+
69
+ def result_is_json_or_unspecified?(result)
70
+ result.headers['Content-Type'].include?('json')
71
+ rescue
72
+ true
73
+ end
74
+
75
+ def translate(result, options = {})
76
+ incoming_content_type = result.headers['Content-Type']
77
+ if result_is_json_or_unspecified?(result)
78
+ result.headers["content-type"] = "application/hal+json"
79
+ end
80
+ result = FaradayResponseProxy.new(OpenStruct.new(status:result.status, response_headers:result.headers, body:result.body))
81
+ if result.body.present?
82
+ if incoming_content_type && incoming_content_type["xml"]
83
+ result.body = yield Crack::XML.parse(result.body)
84
+ else
85
+ result.body = yield MultiJson.load(result.body)
86
+ end
87
+ end
88
+ result
89
+ end
90
+
91
+ def rebuild_request
92
+ if @url != @original_url
93
+ @request.forced_url = @request.url = @url
94
+ end
95
+ if @body != @original_body
96
+ @request.body = @body
97
+ elsif @post_params != @original_post_params
98
+ @request.body = nil
99
+ @request.prepare_request_body(@post_params)
100
+ end
101
+ if @get_params != @original_get_params
102
+ @request.get_params = @get_params
103
+ @request.prepare_url
104
+ @request.append_get_parameters
105
+ end
106
+ end
107
+
108
+ def handle(request, &block)
109
+ @request = request
110
+ @original_handler = block
111
+
112
+ @original_body = request.body
113
+ @body = @original_body.dup
114
+
115
+ @original_get_params = request.get_params
116
+ @get_params = @original_get_params.dup
117
+
118
+ @original_post_params = request.post_params
119
+ @post_params = (@original_post_params || {}).dup
120
+
121
+ @original_url = request.url
122
+ @url = @original_url.dup
123
+
124
+ if mapping = find_mapping_for_current_request
125
+ self.class_eval(&mapping.block)
126
+ else
127
+ passthrough
128
+ end
129
+ end
130
+
131
+ def find_mapping_for_current_request
132
+ uri = URI.parse(@original_url)
133
+ @mappings ||= []
134
+ @params = {}
135
+ @mappings.each do |mapping|
136
+ match = mapping.match
137
+ if (match_data = uri.path.match(match)) && @request.http_method.to_sym == mapping.http_method
138
+ matches = match_data.to_a
139
+ matches.shift
140
+ matches.each_with_index do |value, index|
141
+ @params[mapping.param_keys[index]] = value
142
+ end
143
+ return mapping
144
+ end
145
+ end
146
+ nil
147
+ end
148
+
149
+ def render(body, status=200, content_type="application/javascript", headers={})
150
+ headers["Content-type"] = content_type
151
+ FaradayResponseProxy.new(OpenStruct.new(body:body, status:status, response_headers:headers, proxied:true))
152
+ end
153
+ end
154
+
155
+ def self.inherited(base)
156
+ base.extend(ClassMethods)
157
+ end
158
+ end
159
+
160
+ # FaradayResponseProxy acts just like a Faraday Response object,
161
+ # however it always resolves the request immediately regardless of
162
+ # whether it is inside an in_parallel block or not
163
+ class FaradayResponseProxy
164
+ def initialize(response)
165
+ @response = response
166
+ end
167
+
168
+ def headers
169
+ @response.response_headers
170
+ end
171
+
172
+ def status
173
+ @response.status
174
+ end
175
+
176
+ def body
177
+ @response.body
178
+ end
179
+
180
+ def body=(value)
181
+ @response.body = value
182
+ value
183
+ end
184
+
185
+ def on_complete
186
+ yield(@response)
187
+ end
188
+
189
+ def finished?
190
+ true
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,24 @@
1
+ module Flexirest
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,573 @@
1
+ require "cgi"
2
+ require "multi_json"
3
+ require 'crack'
4
+ require 'crack/xml'
5
+
6
+ module Flexirest
7
+
8
+ class Request
9
+ attr_accessor :post_params, :get_params, :url, :path, :headers, :method, :object, :body, :forced_url, :original_url
10
+
11
+ def initialize(method, object, params = {})
12
+ @method = method
13
+ @method[:options] ||= {}
14
+ @method[:options][:lazy] ||= []
15
+ @method[:options][:has_one] ||= {}
16
+ @overridden_name = @method[:options][:overridden_name]
17
+ @object = object
18
+ @response_delegate = Flexirest::RequestDelegator.new(nil)
19
+ @params = params
20
+ @headers = HeadersList.new
21
+ end
22
+
23
+ def object_is_class?
24
+ !@object.respond_to?(:dirty?)
25
+ end
26
+
27
+ def class_name
28
+ if object_is_class?
29
+ @object.name
30
+ else
31
+ @object.class.name
32
+ end
33
+ end
34
+
35
+ def original_object_class
36
+ if object_is_class?
37
+ @object
38
+ else
39
+ @object.class
40
+ end
41
+ end
42
+
43
+ def base_url
44
+ if object_is_class?
45
+ @object.base_url
46
+ else
47
+ @object.class.base_url
48
+ end
49
+ end
50
+
51
+ def using_api_auth?
52
+ if object_is_class?
53
+ @object.using_api_auth?
54
+ else
55
+ @object.class.using_api_auth?
56
+ end
57
+ end
58
+
59
+ def api_auth_access_id
60
+ if object_is_class?
61
+ @object.api_auth_access_id
62
+ else
63
+ @object.class.api_auth_access_id
64
+ end
65
+ end
66
+
67
+ def api_auth_secret_key
68
+ if object_is_class?
69
+ @object.api_auth_secret_key
70
+ else
71
+ @object.class.api_auth_secret_key
72
+ end
73
+ end
74
+
75
+ def username
76
+ if object_is_class?
77
+ @object.username
78
+ else
79
+ @object.class.username
80
+ end
81
+ end
82
+
83
+ def password
84
+ if object_is_class?
85
+ @object.password
86
+ else
87
+ @object.class.password
88
+ end
89
+ end
90
+
91
+ def request_body_type
92
+ if @method[:options][:request_body_type]
93
+ @method[:options][:request_body_type]
94
+ elsif object_is_class?
95
+ @object.request_body_type
96
+ else
97
+ @object.class.request_body_type
98
+ end
99
+ end
100
+
101
+ def verbose?
102
+ if object_is_class?
103
+ @object.verbose
104
+ else
105
+ @object.class.verbose
106
+ end
107
+ end
108
+
109
+ def translator
110
+ if object_is_class?
111
+ @object.translator
112
+ else
113
+ @object.class.translator
114
+ end
115
+ end
116
+
117
+ def proxy
118
+ if object_is_class?
119
+ @object.proxy
120
+ else
121
+ @object.class.proxy
122
+ end
123
+ rescue
124
+ nil
125
+ end
126
+
127
+ def http_method
128
+ @method[:method]
129
+ end
130
+
131
+ def call(explicit_parameters=nil)
132
+ @instrumentation_name = "#{class_name}##{@method[:name]}"
133
+ result = nil
134
+ cached = nil
135
+ ActiveSupport::Notifications.instrument("request_call.flexirest", :name => @instrumentation_name) do
136
+ @explicit_parameters = explicit_parameters
137
+ @body = nil
138
+ prepare_params
139
+ prepare_url
140
+ if fake = @method[:options][:fake]
141
+ if fake.respond_to?(:call)
142
+ fake = fake.call(self)
143
+ end
144
+ Flexirest::Logger.debug " \033[1;4;32m#{Flexirest::NAME}\033[0m #{@instrumentation_name} - Faked response found"
145
+ content_type = @method[:options][:fake_content_type] || "application/json"
146
+ return handle_response(OpenStruct.new(status:200, body:fake, response_headers:{"X-ARC-Faked-Response" => "true", "Content-Type" => content_type}))
147
+ end
148
+ if object_is_class?
149
+ @object.send(:_filter_request, :before, @method[:name], self)
150
+ else
151
+ @object.class.send(:_filter_request, :before, @method[:name], self)
152
+ end
153
+ append_get_parameters
154
+ prepare_request_body
155
+ self.original_url = self.url
156
+ cached = original_object_class.read_cached_response(self)
157
+ if cached
158
+ if cached.expires && cached.expires > Time.now
159
+ Flexirest::Logger.debug " \033[1;4;32m#{Flexirest::NAME}\033[0m #{@instrumentation_name} - Absolutely cached copy found"
160
+ return handle_cached_response(cached)
161
+ elsif cached.etag.to_s != "" #present? isn't working for some reason
162
+ Flexirest::Logger.debug " \033[1;4;32m#{Flexirest::NAME}\033[0m #{@instrumentation_name} - Etag cached copy found with etag #{cached.etag}"
163
+ etag = cached.etag
164
+ end
165
+ end
166
+
167
+ response = (
168
+ if proxy
169
+ proxy.handle(self) do |request|
170
+ request.do_request(etag)
171
+ end
172
+ else
173
+ do_request(etag)
174
+ end
175
+ )
176
+
177
+ # This block is called immediately when this request is not inside a parallel request block.
178
+ # Otherwise this callback is called after the parallel request block ends.
179
+ response.on_complete do |response_env|
180
+ if verbose?
181
+ Flexirest::Logger.debug " Response"
182
+ Flexirest::Logger.debug " << Status : #{response_env.status}"
183
+ response_env.response_headers.each do |k,v|
184
+ Flexirest::Logger.debug " << #{k} : #{v}"
185
+ end
186
+ Flexirest::Logger.debug " << Body:\n#{response_env.body}"
187
+ end
188
+
189
+ if object_is_class? && @object.record_response?
190
+ @object.record_response(self.url, response_env)
191
+ end
192
+ if object_is_class?
193
+ @object.send(:_filter_request, :after, @method[:name], response_env)
194
+ else
195
+ @object.class.send(:_filter_request, :after, @method[:name], response_env)
196
+ end
197
+
198
+ result = handle_response(response_env, cached)
199
+ @response_delegate.__setobj__(result)
200
+ original_object_class.write_cached_response(self, response_env, result)
201
+ end
202
+
203
+ # If this was not a parallel request just return the original result
204
+ return result if response.finished?
205
+ # Otherwise return the delegate which will get set later once the call back is completed
206
+ return @response_delegate
207
+ end
208
+ end
209
+
210
+ def prepare_params
211
+ params = @params || @object._attributes rescue {}
212
+ if params.is_a?(String) || params.is_a?(Fixnum)
213
+ params = {id:params}
214
+ end
215
+
216
+ default_params = @method[:options][:defaults] || {}
217
+
218
+ if @explicit_parameters
219
+ params = @explicit_parameters
220
+ end
221
+ if http_method == :get
222
+ @get_params = default_params.merge(params || {})
223
+ @post_params = nil
224
+ else
225
+ @post_params = default_params.merge(params || {})
226
+ @get_params = {}
227
+ end
228
+ end
229
+
230
+ def prepare_url
231
+ if @forced_url && @forced_url.present?
232
+ @url = @forced_url
233
+ else
234
+ @url = @method[:url].dup
235
+ matches = @url.scan(/(:[a-z_-]+)/)
236
+ @get_params ||= {}
237
+ @post_params ||= {}
238
+ matches.each do |token|
239
+ token = token.first[1,999]
240
+ 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) || ""
241
+ @url.gsub!(":#{token}", target.to_s)
242
+ end
243
+ end
244
+ end
245
+
246
+ def append_get_parameters
247
+ if @get_params.any?
248
+ @url += "?" + @get_params.to_query
249
+ end
250
+ end
251
+
252
+ def prepare_request_body(params = nil)
253
+ if request_body_type == :form_encoded
254
+ @body ||= (params || @post_params || {}).to_query
255
+ headers["Content-Type"] ||= "application/x-www-form-urlencoded"
256
+ elsif request_body_type == :json
257
+ @body ||= (params || @post_params || {}).to_json
258
+ headers["Content-Type"] ||= "application/json; charset=utf-8"
259
+ end
260
+ end
261
+
262
+ def do_request(etag)
263
+ http_headers = {}
264
+ http_headers["If-None-Match"] = etag if etag
265
+ http_headers["Accept"] = "application/hal+json, application/json;q=0.5"
266
+ headers.each do |key,value|
267
+ value = value.join(",") if value.is_a?(Array)
268
+ http_headers[key] = value
269
+ end
270
+ if @method[:options][:url] || @forced_url
271
+ @url = @method[:options][:url] || @method[:url]
272
+ @url = @forced_url if @forced_url
273
+ if connection = Flexirest::ConnectionManager.find_connection_for_url(@url)
274
+ @url = @url.slice(connection.base_url.length, 255)
275
+ else
276
+ parts = @url.match(%r{^(https?://[a-z\d\.:-]+?)(/.*)}).to_a
277
+ if (parts.empty?) # Not a full URL, so use hostname/protocol from existing base_url
278
+ uri = URI.parse(base_url)
279
+ @base_url = "#{uri.scheme}://#{uri.host}#{":#{uri.port}" if uri.port != 80 && uri.port != 443}"
280
+ @url = "#{base_url}#{@url}".gsub(@base_url, "")
281
+ else
282
+ _, @base_url, @url = parts
283
+ end
284
+ base_url.gsub!(%r{//(.)}, "//#{username}:#{password}@\\1") if username && !base_url[%r{//[^/]*:[^/]*@}]
285
+ connection = Flexirest::ConnectionManager.get_connection(@base_url)
286
+ end
287
+ else
288
+ parts = @url.match(%r{^(https?://[a-z\d\.:-]+?)(/.*)}).to_a
289
+ if (parts.empty?) # Not a full URL, so use hostname/protocol from existing base_url
290
+ uri = URI.parse(base_url)
291
+ @base_url = "#{uri.scheme}://#{uri.host}#{":#{uri.port}" if uri.port != 80 && uri.port != 443}"
292
+ @url = "#{base_url}#{@url}".gsub(@base_url, "")
293
+ base_url = @base_url
294
+ end
295
+ base_url.gsub!(%r{//(.)}, "//#{username}:#{password}@\\1") if username && !base_url[%r{//[^/]*:[^/]*@}]
296
+ connection = Flexirest::ConnectionManager.get_connection(base_url)
297
+ end
298
+ Flexirest::Logger.info " \033[1;4;32m#{Flexirest::NAME}\033[0m #{@instrumentation_name} - Requesting #{connection.base_url}#{@url}"
299
+
300
+ if verbose?
301
+ Flexirest::Logger.debug "Flexirest Verbose Log:"
302
+ Flexirest::Logger.debug " Request"
303
+ Flexirest::Logger.debug " >> #{http_method.upcase} #{@url} HTTP/1.1"
304
+ http_headers.each do |k,v|
305
+ Flexirest::Logger.debug " >> #{k} : #{v}"
306
+ end
307
+ Flexirest::Logger.debug " >> Body:\n#{@body}"
308
+ end
309
+
310
+ request_options = {:headers => http_headers}
311
+ if using_api_auth?
312
+ request_options[:api_auth] = {
313
+ :api_auth_access_id => api_auth_access_id,
314
+ :api_auth_secret_key => api_auth_secret_key
315
+ }
316
+ end
317
+
318
+ case http_method
319
+ when :get
320
+ response = connection.get(@url, request_options)
321
+ when :put
322
+ response = connection.put(@url, @body, request_options)
323
+ when :post
324
+ response = connection.post(@url, @body, request_options)
325
+ when :delete
326
+ response = connection.delete(@url, request_options)
327
+ else
328
+ raise InvalidRequestException.new("Invalid method #{http_method}")
329
+ end
330
+
331
+ response
332
+ end
333
+
334
+ def handle_cached_response(cached)
335
+ if cached.result.is_a? Flexirest::ResultIterator
336
+ cached.result
337
+ else
338
+ if object_is_class?
339
+ cached.result
340
+ else
341
+ @object._copy_from(cached.result)
342
+ @object
343
+ end
344
+ end
345
+ end
346
+
347
+ def handle_response(response, cached = nil)
348
+ @response = response
349
+ status = @response.status || 200
350
+
351
+ if cached && response.status == 304
352
+ Flexirest::Logger.debug " \033[1;4;32m#{Flexirest::NAME}\033[0m #{@instrumentation_name}" +
353
+ ' - Etag copy is the same as the server'
354
+ return handle_cached_response(cached)
355
+ end
356
+
357
+ if (200..399).include?(status)
358
+ if @method[:options][:plain]
359
+ return @response = response.body
360
+ elsif is_json_response? || is_xml_response?
361
+ if @response.respond_to?(:proxied) && @response.proxied
362
+ Flexirest::Logger.debug " \033[1;4;32m#{Flexirest::NAME}\033[0m #{@instrumentation_name} - Response was proxied, unable to determine size"
363
+ else
364
+ Flexirest::Logger.debug " \033[1;4;32m#{Flexirest::NAME}\033[0m #{@instrumentation_name} - Response received #{@response.body.size} bytes"
365
+ end
366
+ result = generate_new_object(ignore_xml_root: @method[:options][:ignore_xml_root])
367
+ else
368
+ raise ResponseParseException.new(status:status, body:@response.body)
369
+ end
370
+ else
371
+ if is_json_response? || is_xml_response?
372
+ error_response = generate_new_object(mutable: false, ignore_xml_root: @method[:options][:ignore_xml_root])
373
+ else
374
+ error_response = @response.body
375
+ end
376
+ if status == 400
377
+ raise HTTPBadRequestClientException.new(status:status, result:error_response, url:@url)
378
+ elsif status == 401
379
+ raise HTTPUnauthorisedClientException.new(status:status, result:error_response, url:@url)
380
+ elsif status == 403
381
+ raise HTTPForbiddenClientException.new(status:status, result:error_response, url:@url)
382
+ elsif status == 404
383
+ raise HTTPNotFoundClientException.new(status:status, result:error_response, url:@url)
384
+ elsif (400..499).include? status
385
+ raise HTTPClientException.new(status:status, result:error_response, url:@url)
386
+ elsif (500..599).include? status
387
+ raise HTTPServerException.new(status:status, result:error_response, url:@url)
388
+ elsif status == 0
389
+ raise TimeoutException.new("Timed out getting #{response.url}")
390
+ end
391
+ end
392
+
393
+ result
394
+ end
395
+
396
+ def new_object(attributes, name = nil)
397
+ @method[:options][:has_many] ||= {}
398
+ name = name.to_sym rescue nil
399
+ if @method[:options][:has_many][name]
400
+ overridden_name = name
401
+ object = @method[:options][:has_many][name].new
402
+ elsif @method[:options][:has_one][name]
403
+ overridden_name = name
404
+ object = @method[:options][:has_one][name].new
405
+ else
406
+ object = create_object_instance
407
+ end
408
+
409
+ if hal_response? && name.nil?
410
+ attributes = handle_hal_links_embedded(object, attributes)
411
+ end
412
+
413
+ attributes.each do |k,v|
414
+ k = k.to_sym
415
+ overridden_name = select_name(k, overridden_name)
416
+ if @method[:options][:lazy].include?(k)
417
+ object._attributes[k] = Flexirest::LazyAssociationLoader.new(overridden_name, v, self, overridden_name:(overridden_name))
418
+ elsif v.is_a? Hash
419
+ object._attributes[k] = new_object(v, overridden_name )
420
+ elsif v.is_a? Array
421
+ object._attributes[k] = Flexirest::ResultIterator.new
422
+ v.each do |item|
423
+ if item.is_a? Hash
424
+ object._attributes[k] << new_object(item, overridden_name)
425
+ else
426
+ object._attributes[k] << item
427
+ end
428
+ end
429
+ else
430
+ if v.to_s[/\d{4}\-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(Z|[+-]\d{2}:\d{2})/]
431
+ object._attributes[k] = DateTime.parse(v)
432
+ else
433
+ object._attributes[k] = v
434
+ end
435
+ end
436
+ end
437
+ object.clean! unless object_is_class?
438
+
439
+ object
440
+ end
441
+
442
+ def hal_response?
443
+ _, content_type = @response.response_headers.detect{|k,v| k.downcase == "content-type"}
444
+ faked_response = @response.response_headers.detect{|k,v| k.downcase == "x-arc-faked-response"}
445
+ if content_type && content_type.respond_to?(:each)
446
+ content_type.each do |ct|
447
+ return true if ct[%r{application\/hal\+json}i]
448
+ return true if ct[%r{application\/json}i]
449
+ end
450
+ faked_response
451
+ elsif content_type && (content_type[%r{application\/hal\+json}i] || content_type[%r{application\/json}i]) || faked_response
452
+ true
453
+ else
454
+ false
455
+ end
456
+ end
457
+
458
+ def handle_hal_links_embedded(object, attributes)
459
+ attributes["_links"] = attributes[:_links] if attributes[:_links]
460
+ attributes["_embedded"] = attributes[:_embedded] if attributes[:_embedded]
461
+ if attributes["_links"]
462
+ attributes["_links"].each do |key, value|
463
+ if value.is_a?(Array)
464
+ object._attributes[key.to_sym] ||= Flexirest::ResultIterator.new
465
+ value.each do |element|
466
+ begin
467
+ embedded_version = attributes["_embedded"][key].detect{|embed| embed["_links"]["self"]["href"] == element["href"]}
468
+ object._attributes[key.to_sym] << new_object(embedded_version, key)
469
+ rescue NoMethodError
470
+ object._attributes[key.to_sym] << Flexirest::LazyAssociationLoader.new(key, element, self)
471
+ end
472
+ end
473
+ else
474
+ begin
475
+ embedded_version = attributes["_embedded"][key]
476
+ object._attributes[key.to_sym] = new_object(embedded_version, key)
477
+ rescue NoMethodError
478
+ object._attributes[key.to_sym] = Flexirest::LazyAssociationLoader.new(key, value, self)
479
+ end
480
+ end
481
+ end
482
+ attributes.delete("_links")
483
+ attributes.delete("_embedded")
484
+ end
485
+
486
+ attributes
487
+ end
488
+
489
+ private
490
+
491
+ def create_object_instance
492
+ return object_is_class? ? @object.new : @object.class.new
493
+ end
494
+
495
+ def select_name(name, parent_name)
496
+ if @method[:options][:has_many][name] || @method[:options][:has_one][name]
497
+ return name
498
+ end
499
+
500
+ parent_name || name
501
+ end
502
+
503
+ def is_json_response?
504
+ @response.response_headers['Content-Type'].nil? || @response.response_headers['Content-Type'].include?('json')
505
+ end
506
+
507
+ def is_xml_response?
508
+ @response.response_headers['Content-Type'].include?('xml')
509
+ end
510
+
511
+ def generate_new_object(options={})
512
+ if @response.body.is_a?(Array) || @response.body.is_a?(Hash)
513
+ body = @response.body
514
+ elsif is_json_response?
515
+ body = @response.body.blank? ? {} : MultiJson.load(@response.body)
516
+ elsif is_xml_response?
517
+ body = @response.body.blank? ? {} : Crack::XML.parse(@response.body)
518
+ if options[:ignore_xml_root]
519
+ body = body[options[:ignore_xml_root].to_s]
520
+ end
521
+ end
522
+ body = begin
523
+ @method[:name].nil? ? body : translator.send(@method[:name], body)
524
+ rescue NoMethodError
525
+ body
526
+ end
527
+ if body.is_a? Array
528
+ result = Flexirest::ResultIterator.new(@response)
529
+ body.each do |json_object|
530
+ result << new_object(json_object, @overridden_name)
531
+ end
532
+ else
533
+ result = new_object(body, @overridden_name)
534
+ result._status = @response.status
535
+ result._headers = @response.response_headers
536
+ result._etag = @response.response_headers['ETag']
537
+ if !object_is_class? && options[:mutable] != false
538
+ @object._copy_from(result)
539
+ @object._clean!
540
+ result = @object
541
+ end
542
+ end
543
+ result
544
+ end
545
+ end
546
+
547
+ class RequestException < StandardError ; end
548
+
549
+ class InvalidRequestException < RequestException ; end
550
+ class ResponseParseException < RequestException
551
+ attr_accessor :status, :body
552
+ def initialize(options)
553
+ @status = options[:status]
554
+ @body = options[:body]
555
+ end
556
+ end
557
+
558
+ class HTTPException < RequestException
559
+ attr_accessor :status, :result, :request_url
560
+ def initialize(options)
561
+ @status = options[:status]
562
+ @result = options[:result]
563
+ @request_url = options[:url]
564
+ end
565
+ end
566
+ class HTTPClientException < HTTPException ; end
567
+ class HTTPUnauthorisedClientException < HTTPClientException ; end
568
+ class HTTPBadRequestClientException < HTTPClientException ; end
569
+ class HTTPForbiddenClientException < HTTPClientException ; end
570
+ class HTTPNotFoundClientException < HTTPClientException ; end
571
+ class HTTPServerException < HTTPException ; end
572
+
573
+ end