flexirest 1.3.35 → 1.4.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.
@@ -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"