flexirest 1.3.35 → 1.4.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -4,6 +4,7 @@ module Flexirest
4
4
  module Configuration
5
5
  module ClassMethods
6
6
  @@base_url = nil
7
+ @@alias_type = nil
7
8
  @@username = nil
8
9
  @@password = nil
9
10
  @@request_body_type = :form_encoded
@@ -92,6 +93,23 @@ module Flexirest
92
93
  @@password = value
93
94
  end
94
95
 
96
+ def alias_type(value = nil)
97
+ @alias_type ||= nil
98
+ if value.nil?
99
+ if @alias_type.nil?
100
+ if value.nil? && superclass.respond_to?(:alias_type)
101
+ superclass.alias_type
102
+ else
103
+ @@alias_type || nil
104
+ end
105
+ else
106
+ @alias_type
107
+ end
108
+ else
109
+ @alias_type = value
110
+ end
111
+ end
112
+
95
113
  def request_body_type(value = nil)
96
114
  @request_body_type ||= nil
97
115
  if value.nil?
@@ -231,6 +249,13 @@ module Flexirest
231
249
  def proxy(value = nil)
232
250
  @proxy ||= nil
233
251
  value ? @proxy = value : @proxy || nil
252
+
253
+ if !@proxy.nil?
254
+ return @proxy
255
+ elsif self.superclass.respond_to?(:proxy)
256
+ return self.superclass.proxy
257
+ end
258
+ nil
234
259
  end
235
260
 
236
261
  def _reset_configuration!
@@ -0,0 +1,398 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Flexirest
4
+ # JSON API requests and responses
5
+ module JsonAPIProxy
6
+ @object ||= nil
7
+ @headers ||= {}
8
+
9
+ # Methods used across other modules
10
+ module Helpers
11
+ def singular?(word)
12
+ w = word.to_s
13
+ w.singularize == w && w.pluralize != w
14
+ end
15
+
16
+ def type(object)
17
+ # Retrieve the type value for JSON API from the Flexirest::Base class
18
+ # If `alias_type` has been defined within the class, use it
19
+ name = object.alias_type || object.class.alias_type
20
+
21
+ # If not, guess the type value from the class name itself
22
+ unless name
23
+ return object.class.name.underscore.split('/').last.pluralize
24
+ end
25
+
26
+ name
27
+ end
28
+ end
29
+
30
+ # Creating JSON API requests
31
+ module Request
32
+ # Creating and formatting JSON API parameters
33
+ module Params
34
+ extend self
35
+ extend Flexirest::JsonAPIProxy::Helpers
36
+
37
+ def create(params, object)
38
+ # Create a parameters object with the resource's type value and id
39
+ parameters = Parameters.new(object.id, type(object))
40
+
41
+ # Remove id attribute from top-level hash, this will be included
42
+ # in the resource object
43
+ params.delete(:id)
44
+
45
+ # Build the JSON API compliant parameters
46
+ parameters.create_from_hash(params)
47
+
48
+ # Return the parameters as a hash, so it can be used elsewhere
49
+ parameters.to_hash
50
+ end
51
+
52
+ def translate(params, include_associations)
53
+ # Return to caller if nothing is to be done
54
+ return params unless params.present? && include_associations.present?
55
+
56
+ # Format the linked resources array, and assign to include key
57
+ params[:include] = format_include_params(include_associations)
58
+ end
59
+
60
+ private
61
+
62
+ def format_include_params(associations)
63
+ includes = []
64
+
65
+ associations.each do |key|
66
+ # Format each association name
67
+ # if the key is a nested hash, format each nested association too
68
+ # e.g. [author, comments.likes]
69
+
70
+ if key.is_a?(Hash)
71
+ # Create a link from each association to nested association
72
+ key.each { |k, val| val.each { |v| includes << "#{k}.#{v}" } }
73
+
74
+ else
75
+ # Just convert the association to string, in case it is a Symbol
76
+ includes << key.to_s
77
+ end
78
+ end
79
+
80
+ # Join the includes array with comma separator
81
+ includes.join(',')
82
+ end
83
+
84
+ # Private class for building JSON API compliant parameters
85
+ class Parameters
86
+ include Flexirest::JsonAPIProxy::Helpers
87
+
88
+ def initialize(id, type)
89
+ @params = build(id, type)
90
+ end
91
+
92
+ def to_hash
93
+ @params
94
+ end
95
+
96
+ def create_from_hash(hash)
97
+ hash.each do |k, v|
98
+ # Build JSON API compliant parameters from each key and value
99
+ # in the standard-style parameters hash
100
+
101
+ if v.is_a?(Array)
102
+ # This is a one-to-many relationship
103
+ validate_relationships!(v)
104
+
105
+ # Add a relationship object for all related resources
106
+ v.each { |el| add_relationship(k, type(el), el.id) }
107
+
108
+ elsif v.is_a?(Flexirest::Base)
109
+ # This is a one-to-one relationship
110
+ add_relationship(k, type(v), v.id)
111
+
112
+ else
113
+ # This is a normal attribute
114
+ add_attribute(k, v)
115
+ end
116
+ end
117
+ end
118
+
119
+ def add_relationship(name, type, id)
120
+ # Use the `name` parameter to determine the type of relationship
121
+
122
+ if singular?(name)
123
+ # If `name` is a singular word (one-to-one relationship),
124
+ # add or overwrite the data object for the given `name`,
125
+ # containing a type and id value to the relationships object
126
+ @params[:data][:relationships][name] =
127
+ { data: { type: type, id: id } }
128
+
129
+ elsif @params[:data][:relationships][name]
130
+ # If `name` is a plural word (one-to-many relationship),
131
+ # and the `name` object already exists in the relationships object,
132
+ # assume a nested data array exists, and add a new data object
133
+ # containing a type and id value to the data array
134
+ @params[:data][:relationships][name][:data] <<
135
+ { type: type, id: id }
136
+
137
+ else
138
+ # If `name` is a plural word, but the `name` object does not exist,
139
+ # add a new `name` object containing a data array,
140
+ # which consists of exactly one data object with the type and id
141
+ @params[:data][:relationships][name] =
142
+ { data: [{ type: type, id: id }] }
143
+ end
144
+ end
145
+
146
+ def add_attribute(key, value)
147
+ # Add a resource attribute to the attributes object
148
+ # within the resource object
149
+ @params[:data][:attributes][key] = value
150
+ end
151
+
152
+ def build(id, type)
153
+ # Build the standard resource object
154
+ pp = {}
155
+ pp[:data] = {}
156
+ pp[:data][:id] = id if id
157
+ pp[:data][:type] = type
158
+ pp[:data][:attributes] = {}
159
+ pp[:data][:relationships] = {}
160
+ pp
161
+ end
162
+
163
+ def validate_relationships!(v)
164
+ # Should always contain the same class in entire relationships array
165
+ raise_params_error! if v.map(&:class).count > 1
166
+ end
167
+
168
+ def raise_params_error!
169
+ raise Exception.new("Cannot contain different instance types!")
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ # Creating JSON API header
176
+ module Headers
177
+ extend self
178
+ def save(headers)
179
+ # Save headers used in a request for building lazy association
180
+ # loaders when parsing the response
181
+ @headers = headers
182
+ end
183
+ end
184
+
185
+ # Parsing JSON API responses
186
+ module Response
187
+ extend self
188
+ extend Flexirest::JsonAPIProxy::Helpers
189
+
190
+ def save_resource_class(object)
191
+ @resource_class = object.is_a?(Class) ? object : object.class
192
+ end
193
+
194
+ def parse(body, object)
195
+ # Save resource class for building lazy association loaders
196
+ save_resource_class(object)
197
+
198
+ # Retrieve the resource(s) object or array from the data object
199
+ records = body['data']
200
+ return records unless records.present?
201
+
202
+ # Convert the resource object to an array,
203
+ # because it is easier to work with an array than a single object
204
+ # Also keep track if record is singular or plural for the result later
205
+ is_singular_record = records.is_a?(Hash)
206
+ records = [records] if is_singular_record
207
+
208
+ # Retrieve all names of linked relationships
209
+ if records.first['relationships']
210
+ relationships = records.first['relationships'].keys
211
+ end
212
+
213
+ resource_type = records.first['type']
214
+ included = body['included']
215
+
216
+ # Parse the records, and retrieve all resources in a
217
+ # (nested) array of resources that is easy to work with in Flexirest
218
+ resources = records.map do |record|
219
+ fetch_attributes_and_relationships(
220
+ resource_type, record, included, relationships
221
+ )
222
+ end
223
+
224
+ # Depending on whether we got a resource object (hash) or array
225
+ # in the beginning, return to the caller with the same type
226
+ is_singular_record ? resources.first : resources
227
+ end
228
+
229
+ private
230
+
231
+ def fetch_attributes_and_relationships(base, record, included, rels)
232
+ rels = (rels || []) - [base]
233
+ rels_object = record['relationships']
234
+
235
+ rels.each do |rel_name|
236
+ # Determine from `rel_name` (relationship name) whether the
237
+ # linked resource is a singular or plural (one-to-one or
238
+ # one-to-many, respectively)
239
+ is_singular_rel = singular?(rel_name)
240
+
241
+ if is_singular_rel
242
+ # Fetch a linked resource from the relationships object
243
+ # and add it as an association attribute in the resource hash
244
+ record[rel_name], embedded = fetch_one_to_one(
245
+ rels_object, rel_name, included
246
+ )
247
+
248
+ else
249
+ # Fetch linked resources from the relationships object
250
+ # and add it as an array into the resource hash
251
+ record[rel_name], embedded = fetch_one_to_many(
252
+ rels_object, rel_name, included
253
+ )
254
+ end
255
+
256
+ # Do not try to fetch embedded results if the response is not
257
+ # a compound document. Instead, a LazyAssociationLoader should
258
+ # have been created and inserted into the record
259
+ next record unless embedded
260
+
261
+ # Recursively fetch the relationships and embedded nested resources
262
+ linked_resources = record[rel_name].map do |nested_record|
263
+ # Find the relationships object in the linked resource
264
+ # and find whether there are any nested linked resources
265
+ nested_rels_object = nested_record['relationships']
266
+
267
+ if nested_rels_object && nested_rels_object.keys.present?
268
+ # Fetch the linked resources and its attributes recursively
269
+ fetch_attributes_and_relationships(
270
+ record['type'], nested_record, included, nested_rels_object.keys
271
+ )
272
+
273
+ else
274
+ # If there are no nested linked resources, just fetch the
275
+ # resource attributes of the linked resource
276
+ fetch_and_delete_attributes(nested_record)
277
+ end
278
+ end
279
+
280
+ # Depending on if the resource is singular or plural, add it as
281
+ # the original type (array or hash) into the record hash
282
+ record[rel_name] =
283
+ if is_singular_rel
284
+ linked_resources.first
285
+ else
286
+ linked_resources
287
+ end
288
+ end
289
+
290
+ # Add the record attributes to the record hash
291
+ fetch_and_delete_attributes(record)
292
+ record
293
+ end
294
+
295
+ def fetch_one_to_one(relationships, name, included)
296
+ # Parse the relationships object given the relationship name `name`,
297
+ # and look into the included object (in case of a compound document),
298
+ # to embed the linked resource into the response
299
+
300
+ if included.blank? || relationships[name]['data'].blank?
301
+ begin
302
+ # When the response is not a compound document (i.e. there is no
303
+ # includes object), build a LazyAssociationLoader for lazy loading
304
+ return build_lazy_loader(
305
+ name, relationships[name]['links']['related']
306
+ ), false
307
+ rescue NoMethodError
308
+ # If the url for retrieving the linked resource is missing,
309
+ # we assume there is no linked resource available to fetch
310
+ # Default nulled linked resource is `nil`
311
+ return nil, false
312
+ end
313
+ end
314
+
315
+ # Retrieve the linked resource id and its pluralized type name
316
+ rel_id = relationships[name]['data']['id']
317
+ plural_name = name.pluralize
318
+
319
+ # Traverse through the included object, and find the included
320
+ # linked resource, based on the given id and pluralized type name
321
+ linked_resource = included.select do |i|
322
+ i['id'] == rel_id && i['type'] == plural_name
323
+ end
324
+
325
+ return linked_resource, true
326
+ end
327
+
328
+ def fetch_one_to_many(relationships, name, included)
329
+ # Parse the relationships object given the relationship name `name`,
330
+ # and look into the included object (in case of a compound document),
331
+ # to embed the linked resources into the response
332
+
333
+ if included.blank? || relationships[name]['data'].blank?
334
+ begin
335
+ # When the response is not a compound document (i.e. there is no
336
+ # includes object), build a LazyAssociationLoader for lazy loading
337
+ return build_lazy_loader(
338
+ name, relationships[name]['links']['related']
339
+ ), false
340
+ rescue NoMethodError
341
+ # If the url for retrieving the linked resources is missing,
342
+ # we assume there are no linked resources available to fetch
343
+ # Default nulled linked resources is an empty array
344
+ return [], false
345
+ end
346
+ end
347
+
348
+ # Retrieve the linked resources ids
349
+ rel_ids = relationships[name]['data'].map { |r| r['id'] }
350
+
351
+ # Traverse through the included object, and find the included
352
+ # linked resources, based on the given ids and type name
353
+ linked_resources = included.select do |i|
354
+ rel_ids.include?(i['id']) && i['type'] == name
355
+ end
356
+
357
+ return linked_resources, true
358
+ end
359
+
360
+ def fetch_and_delete_attributes(record)
361
+ # Fetch attribute keys and values from the resource object
362
+ # and insert into result record hash
363
+ record['attributes'].each do |k, v|
364
+ record[k] = v
365
+ end
366
+
367
+ delete_keys(record)
368
+ record
369
+ end
370
+
371
+ def delete_keys(record)
372
+ # Delete the attribute keys and values from the original response hash
373
+ record.delete('type')
374
+ record.delete('links')
375
+ record.delete('attributes')
376
+ record.delete('relationships')
377
+ end
378
+
379
+ def build_lazy_loader(name, url)
380
+ # Create a new request, given the linked resource `name`,
381
+ # finding the association's class, and given the `url` to the linked
382
+ # resource
383
+
384
+ request = Flexirest::Request.new(
385
+ { url: url, method: :get },
386
+ @resource_class._associations[name.to_sym].new
387
+ )
388
+
389
+ # Also add the previous request's header, which may contain
390
+ # crucial authentication headers (or so), to connect with the service
391
+ request.headers = @headers
392
+ request.url = request.forced_url = url
393
+
394
+ Flexirest::LazyAssociationLoader.new(name, url, request)
395
+ end
396
+ end
397
+ end
398
+ end
@@ -51,14 +51,20 @@ module Flexirest
51
51
  yield loader
52
52
  end
53
53
  elsif @subloaders.is_a? Hash
54
- @subloaders.each do |key,value|
54
+ @subloaders.each do |key, value|
55
55
  yield key, value
56
56
  end
57
57
  end
58
58
  else
59
59
  ensure_lazy_loaded
60
- @object.each do |obj|
61
- yield obj
60
+ if @object.is_a? Flexirest::Base
61
+ @object.each do |key, value|
62
+ yield key, value
63
+ end
64
+ else
65
+ @object.each do |obj|
66
+ yield obj
67
+ end
62
68
  end
63
69
  end
64
70
  end
@@ -17,7 +17,7 @@ module Flexirest
17
17
  def put(match, &block)
18
18
  add_mapping(:put, match, block)
19
19
  end
20
-
20
+
21
21
  def patch(match, &block)
22
22
  add_mapping(:patch, match, block)
23
23
  end
@@ -7,6 +7,7 @@ module Flexirest
7
7
 
8
8
  class Request
9
9
  include AttributeParsing
10
+ include JsonAPIProxy
10
11
  attr_accessor :post_params, :get_params, :url, :path, :headers, :method, :object, :body, :forced_url, :original_url
11
12
 
12
13
  def initialize(method, object, params = {})
@@ -106,6 +107,8 @@ module Flexirest
106
107
  def request_body_type
107
108
  if @method[:options][:request_body_type]
108
109
  @method[:options][:request_body_type]
110
+ elsif @object.nil?
111
+ nil
109
112
  elsif object_is_class?
110
113
  @object.request_body_type
111
114
  else
@@ -180,7 +183,7 @@ module Flexirest
180
183
  end
181
184
 
182
185
  response = (
183
- if proxy
186
+ if proxy && proxy.is_a?(Class)
184
187
  proxy.handle(self) do |request|
185
188
  request.do_request(etag)
186
189
  end
@@ -228,6 +231,12 @@ module Flexirest
228
231
  params = {id:params}
229
232
  end
230
233
 
234
+ # Format includes parameter for jsonapi
235
+ if proxy == :json_api
236
+ JsonAPIProxy::Request::Params.translate(params, @object._include_associations)
237
+ @object._reset_include_associations!
238
+ end
239
+
231
240
  if @method[:options][:defaults].respond_to?(:call)
232
241
  default_params = @method[:options][:defaults].call(params)
233
242
  else
@@ -323,7 +332,17 @@ module Flexirest
323
332
  end
324
333
 
325
334
  def prepare_request_body(params = nil)
326
- if http_method == :get
335
+ if proxy == :json_api
336
+ if http_method == :get || http_method == :delete
337
+ @body = ""
338
+ else
339
+ headers["Content-Type"] ||= "application/vnd.api+json"
340
+ @body = JsonAPIProxy::Request::Params.create(params || @post_params || {}, @object).to_json
341
+ end
342
+
343
+ headers["Accept"] ||= "application/vnd.api+json"
344
+ JsonAPIProxy::Headers.save(headers)
345
+ elsif http_method == :get
327
346
  @body = ""
328
347
  elsif request_body_type == :form_encoded
329
348
  @body ||= (params || @post_params || {}).to_query
@@ -475,7 +494,6 @@ module Flexirest
475
494
  raise TimeoutException.new("Timed out getting #{response.url}")
476
495
  end
477
496
  end
478
-
479
497
  result
480
498
  end
481
499
 
@@ -600,6 +618,10 @@ module Flexirest
600
618
  @response.response_headers['Content-Type'].nil? || @response.response_headers['Content-Type'].include?('json')
601
619
  end
602
620
 
621
+ def is_json_api_response?
622
+ @response.response_headers['Content-Type'] && @response.response_headers['Content-Type'].include?('application/vnd.api+json')
623
+ end
624
+
603
625
  def is_xml_response?
604
626
  @response.response_headers['Content-Type'].include?('xml')
605
627
  end
@@ -610,10 +632,14 @@ module Flexirest
610
632
  elsif is_json_response?
611
633
  begin
612
634
  body = @response.body.blank? ? {} : MultiJson.load(@response.body)
613
- rescue MultiJson::ParseError => exception
635
+ rescue MultiJson::ParseError
614
636
  raise ResponseParseException.new(status:@response.status, body:@response.body, headers:@response.headers)
615
637
  end
616
638
 
639
+ if is_json_api_response?
640
+ body = JsonAPIProxy::Response.parse(body, @object)
641
+ end
642
+
617
643
  if options[:ignore_root]
618
644
  body = body[options[:ignore_root].to_s]
619
645
  end
@@ -1,3 +1,3 @@
1
1
  module Flexirest
2
- VERSION = "1.3.35"
2
+ VERSION = "1.4.0"
3
3
  end
data/lib/flexirest.rb CHANGED
@@ -13,6 +13,7 @@ require "flexirest/result_iterator"
13
13
  require "flexirest/headers_list"
14
14
  require "flexirest/lazy_loader"
15
15
  require "flexirest/lazy_association_loader"
16
+ require "flexirest/json_api_proxy"
16
17
  require "flexirest/request"
17
18
  require "flexirest/request_delegator"
18
19
  require "flexirest/validation"