jsonapi-consumer 0.1.1 → 1.0.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 (76) hide show
  1. checksums.yaml +5 -5
  2. data/.circleci/config.yml +27 -0
  3. data/.gitignore +1 -0
  4. data/Gemfile +6 -4
  5. data/README.md +9 -38
  6. data/Rakefile +17 -6
  7. data/bin/console +14 -0
  8. data/bin/setup +8 -0
  9. data/jsonapi-consumer.gemspec +10 -11
  10. data/lib/jsonapi/consumer/associations/base_association.rb +26 -0
  11. data/lib/jsonapi/consumer/associations/belongs_to.rb +30 -0
  12. data/lib/jsonapi/consumer/associations/has_many.rb +26 -0
  13. data/lib/jsonapi/consumer/associations/has_one.rb +19 -0
  14. data/lib/jsonapi/consumer/connection.rb +36 -0
  15. data/lib/jsonapi/consumer/error_collector.rb +91 -0
  16. data/lib/jsonapi/consumer/errors.rb +34 -76
  17. data/lib/jsonapi/consumer/formatter.rb +145 -0
  18. data/lib/jsonapi/consumer/helpers/callbacks.rb +27 -0
  19. data/lib/jsonapi/consumer/helpers/dirty.rb +71 -0
  20. data/lib/jsonapi/consumer/helpers/dynamic_attributes.rb +83 -0
  21. data/lib/jsonapi/consumer/helpers/uri.rb +9 -0
  22. data/lib/jsonapi/consumer/implementation.rb +12 -0
  23. data/lib/jsonapi/consumer/included_data.rb +49 -0
  24. data/lib/jsonapi/consumer/linking/links.rb +22 -0
  25. data/lib/jsonapi/consumer/linking/top_level_links.rb +39 -0
  26. data/lib/jsonapi/consumer/meta_data.rb +19 -0
  27. data/lib/jsonapi/consumer/middleware/json_request.rb +26 -0
  28. data/lib/jsonapi/consumer/middleware/parse_json.rb +22 -23
  29. data/lib/jsonapi/consumer/middleware/status.rb +41 -0
  30. data/lib/jsonapi/consumer/paginating/paginator.rb +89 -0
  31. data/lib/jsonapi/consumer/parsers/parser.rb +113 -0
  32. data/lib/jsonapi/consumer/query/builder.rb +212 -0
  33. data/lib/jsonapi/consumer/query/requestor.rb +67 -0
  34. data/lib/jsonapi/consumer/relationships/relations.rb +56 -0
  35. data/lib/jsonapi/consumer/relationships/top_level_relations.rb +30 -0
  36. data/lib/jsonapi/consumer/resource.rb +514 -54
  37. data/lib/jsonapi/consumer/result_set.rb +25 -0
  38. data/lib/jsonapi/consumer/schema.rb +153 -0
  39. data/lib/jsonapi/consumer/utils.rb +28 -0
  40. data/lib/jsonapi/consumer/version.rb +1 -1
  41. data/lib/jsonapi/consumer.rb +59 -34
  42. metadata +51 -111
  43. data/.rspec +0 -2
  44. data/CHANGELOG.md +0 -36
  45. data/lib/jsonapi/consumer/middleware/raise_error.rb +0 -21
  46. data/lib/jsonapi/consumer/middleware/request_headers.rb +0 -20
  47. data/lib/jsonapi/consumer/middleware/request_timeout.rb +0 -9
  48. data/lib/jsonapi/consumer/middleware.rb +0 -5
  49. data/lib/jsonapi/consumer/parser.rb +0 -75
  50. data/lib/jsonapi/consumer/query/base.rb +0 -34
  51. data/lib/jsonapi/consumer/query/create.rb +0 -9
  52. data/lib/jsonapi/consumer/query/delete.rb +0 -10
  53. data/lib/jsonapi/consumer/query/find.rb +0 -16
  54. data/lib/jsonapi/consumer/query/new.rb +0 -15
  55. data/lib/jsonapi/consumer/query/update.rb +0 -11
  56. data/lib/jsonapi/consumer/query.rb +0 -5
  57. data/lib/jsonapi/consumer/resource/association_concern.rb +0 -203
  58. data/lib/jsonapi/consumer/resource/attributes_concern.rb +0 -70
  59. data/lib/jsonapi/consumer/resource/connection_concern.rb +0 -99
  60. data/lib/jsonapi/consumer/resource/finders_concern.rb +0 -28
  61. data/lib/jsonapi/consumer/resource/object_build_concern.rb +0 -28
  62. data/lib/jsonapi/consumer/resource/serializer_concern.rb +0 -63
  63. data/spec/fixtures/.gitkeep +0 -0
  64. data/spec/fixtures/resources.rb +0 -45
  65. data/spec/fixtures/responses.rb +0 -64
  66. data/spec/jsonapi/consumer/associations_spec.rb +0 -166
  67. data/spec/jsonapi/consumer/attributes_spec.rb +0 -27
  68. data/spec/jsonapi/consumer/connection_spec.rb +0 -147
  69. data/spec/jsonapi/consumer/error_handling_spec.rb +0 -37
  70. data/spec/jsonapi/consumer/object_build_spec.rb +0 -20
  71. data/spec/jsonapi/consumer/parser_spec.rb +0 -39
  72. data/spec/jsonapi/consumer/resource_spec.rb +0 -62
  73. data/spec/jsonapi/consumer/serializer_spec.rb +0 -41
  74. data/spec/spec_helper.rb +0 -97
  75. data/spec/support/.gitkeep +0 -0
  76. data/spec/support/load_fixtures.rb +0 -4
@@ -1,92 +1,552 @@
1
+ require 'forwardable'
2
+ require 'active_support/all'
3
+ require 'active_model'
4
+
1
5
  module JSONAPI::Consumer
2
- module Resource
3
- extend ActiveSupport::Concern
6
+ class Resource
7
+ extend ActiveModel::Naming
8
+ extend ActiveModel::Translation
9
+ include ActiveModel::Validations
10
+ include ActiveModel::Conversion
11
+ include ActiveModel::Serialization
4
12
 
5
- included do
6
- extend ActiveModel::Naming
13
+ include Helpers::DynamicAttributes
14
+ include Helpers::Dirty
7
15
 
8
- attr_reader :errors
9
- class_attribute :host, :connection, :ssl
10
- end
16
+ attr_accessor :last_result_set,
17
+ :links,
18
+ :relationships
19
+ class_attribute :site,
20
+ :primary_key,
21
+ :parser,
22
+ :paginator,
23
+ :connection_class,
24
+ :connection_object,
25
+ :connection_options,
26
+ :query_builder,
27
+ :linker,
28
+ :relationship_linker,
29
+ :read_only_attributes,
30
+ :requestor_class,
31
+ :associations,
32
+ :json_key_format,
33
+ :route_format,
34
+ instance_accessor: false
35
+ self.primary_key = :id
36
+ self.parser = Parsers::Parser
37
+ self.paginator = Paginating::Paginator
38
+ self.connection_class = Connection
39
+ self.connection_options = {}
40
+ self.query_builder = Query::Builder
41
+ self.linker = Linking::Links
42
+ self.relationship_linker = Relationships::Relations
43
+ self.read_only_attributes = [:id, :type, :links, :meta, :relationships]
44
+ self.requestor_class = Query::Requestor
45
+ self.associations = []
46
+
47
+ #:underscored_key, :camelized_key, :dasherized_key, or custom
48
+ self.json_key_format = :underscored_key
49
+
50
+ #:underscored_route, :camelized_route, :dasherized_route, or custom
51
+ self.route_format = :underscored_route
52
+
53
+ include Associations::BelongsTo
54
+ include Associations::HasMany
55
+ include Associations::HasOne
56
+
57
+ class << self
58
+ extend Forwardable
59
+ def_delegators :_new_scope, :where, :order, :includes, :select, :all, :paginate, :page, :with_params, :first, :find, :last
60
+
61
+ # The table name for this resource. i.e. Article -> articles, Person -> people
62
+ #
63
+ # @return [String] The table name for this resource
64
+ def table_name
65
+ route_formatter.format(resource_name.pluralize)
66
+ end
67
+
68
+ # The name of a single resource. i.e. Article -> article, Person -> person
69
+ #
70
+ # @return [String]
71
+ def resource_name
72
+ name.demodulize.underscore
73
+ end
74
+
75
+ # Specifies the JSON API resource type. By default this is inferred
76
+ # from the resource class name.
77
+ #
78
+ # @return [String] Resource path
79
+ def type
80
+ table_name
81
+ end
82
+
83
+ # Specifies the relative path that should be used for this resource;
84
+ # by default, this is inferred from the resource class name.
85
+ #
86
+ # @return [String] Resource path
87
+ def resource_path
88
+ table_name
89
+ end
90
+
91
+ # Load a resource object from attributes and consider it persisted
92
+ #
93
+ # @return [Resource] Persisted resource object
94
+ def load(params)
95
+ new(params).tap do |resource|
96
+ resource.mark_as_persisted!
97
+ resource.clear_changes_information
98
+ end
99
+ end
100
+
101
+ # Return/build a connection object
102
+ #
103
+ # @return [Connection] The connection to the json api server
104
+ def connection(rebuild = false, &block)
105
+ _build_connection(rebuild, &block)
106
+ connection_object
107
+ end
108
+
109
+ # Param names that will be considered path params. They will be used
110
+ # to build the resource path rather than treated as attributes
111
+ #
112
+ # @return [Array] Param name symbols of parameters that will be treated as path parameters
113
+ def prefix_params
114
+ _belongs_to_associations.map(&:param)
115
+ end
116
+
117
+ # Return the path or path pattern for this resource
118
+ def path(params = nil)
119
+ parts = [resource_path]
120
+ if params && _prefix_path.present?
121
+ path_params = params.delete(:path) || params
122
+ parts.unshift(_set_prefix_path(path_params.symbolize_keys))
123
+ else
124
+ parts.unshift(_prefix_path)
125
+ end
126
+ parts.reject!(&:blank?)
127
+ File.join(*parts)
128
+ rescue KeyError
129
+ raise ArgumentError, "Not all prefix parameters specified"
130
+ end
131
+
132
+ # Create a new instance of this resource class
133
+ #
134
+ # @param attributes [Hash] The attributes to create this resource with
135
+ # @return [Resource] The instance you tried to create. You will have to check the persisted state or errors on this object to see success/failure.
136
+ def create(attributes = {})
137
+ new(attributes).tap do |resource|
138
+ resource.save
139
+ end
140
+ end
141
+
142
+ # Within the given block, add these headers to all requests made by
143
+ # the resource class
144
+ #
145
+ # @param headers [Hash] The headers to send along
146
+ # @param block [Block] The block where headers will be set for
147
+ def with_headers(headers)
148
+ self._custom_headers = headers
149
+ yield
150
+ ensure
151
+ self._custom_headers = {}
152
+ end
153
+
154
+ # The current custom headers to send with any request made by this
155
+ # resource class. This supports inheritance so it only needs to be
156
+ # set on the base class.
157
+ #
158
+ # @return [Hash] Headers
159
+ def custom_headers
160
+ return _header_store.to_h if superclass == Object
161
+
162
+ superclass.custom_headers.merge(_header_store.to_h)
163
+ end
164
+
165
+ # Run a command wrapped in an Authorization header
166
+ #
167
+ def authorize_with(jwt, &block)
168
+ with_headers(authorization: %(Bearer #{jwt}), &block)
169
+ end
170
+
171
+ # Set the Authorization header to a JWT value
172
+ #
173
+ def authorize_with=(jwt)
174
+ if jwt.nil?
175
+ self._custom_headers = {authorization: nil}
176
+ else
177
+ self._custom_headers = {authorization: %(Bearer #{jwt})}
178
+ end
179
+ end
180
+
181
+ # Clears the Authorization header
182
+ #
183
+ def clear_authorization!
184
+ self.authorize_with = nil
185
+ end
186
+
187
+ # @return [String] The Authorization header
188
+ def authorized_as
189
+ custom_headers[:authorization]
190
+ end
191
+
192
+ # Returns based on the presence of an Authorization header
193
+ #
194
+ # @return [Boolean]
195
+ def authorized?
196
+ !custom_headers[:authorization].nil?
197
+ end
198
+
199
+ # Returns the requestor for this resource class
200
+ #
201
+ # @return [Requestor] The requestor for this resource class
202
+ def requestor
203
+ @requestor ||= requestor_class.new(self)
204
+ end
205
+
206
+ # Default attributes that every instance of this resource should be
207
+ # initialized with. Optionally, override this method in a subclass.
208
+ #
209
+ # @return [Hash] Default attributes
210
+ def default_attributes
211
+ {type: type}
212
+ end
213
+
214
+ # Returns the schema for this resource class
215
+ #
216
+ # @return [Schema] The schema for this resource class
217
+ def schema
218
+ @schema ||= Schema.new
219
+ end
220
+
221
+ def key_formatter
222
+ JSONAPI::Consumer::Formatter.formatter_for(json_key_format)
223
+ end
224
+
225
+ def route_formatter
226
+ JSONAPI::Consumer::Formatter.formatter_for(route_format)
227
+ end
228
+
229
+ protected
230
+
231
+ # Declares a new class/instance method that acts on the collection/member
232
+ #
233
+ # @param name [Symbol] the name of the endpoint
234
+ # @param options [Hash] endpoint options
235
+ # @option [Symbol] :on One of [:collection or :member] to decide whether it's a collect or member method
236
+ # @option [Symbol] :request_method The request method (:get, :post, etc)
237
+ def custom_endpoint(name, options = {})
238
+ if :collection == options.delete(:on)
239
+ collection_endpoint(name, options)
240
+ else
241
+ member_endpoint(name, options)
242
+ end
243
+ end
244
+
245
+ # Declares a new class method that acts on the collection
246
+ #
247
+ # @param name [Symbol] the name of the endpoint and the method name
248
+ # @param options [Hash] endpoint options
249
+ # @option options [Symbol] :request_method The request method (:get, :post, etc)
250
+ def collection_endpoint(name, options = {})
251
+ metaclass = class << self
252
+ self
253
+ end
254
+ metaclass.instance_eval do
255
+ define_method(name) do |*params|
256
+ request_params = params.first || {}
257
+ requestor.custom(name, options, request_params)
258
+ end
259
+ end
260
+ end
11
261
 
12
- include ObjectBuildConcern
13
- include AttributesConcern
14
- include AssociationConcern
15
- include FindersConcern
16
- include SerializerConcern
17
- include ConnectionConcern
262
+ # Declares a new instance method that acts on the member object
263
+ #
264
+ # @param name [Symbol] the name of the endpoint and the method name
265
+ # @param options [Hash] endpoint options
266
+ # @option options [Symbol] :request_method The request method (:get, :post, etc)
267
+ def member_endpoint(name, options = {})
268
+ define_method name do |*params|
269
+ request_params = params.first || {}
270
+ request_params[self.class.primary_key] = attributes.fetch(self.class.primary_key)
271
+ self.class.requestor.custom(name, options, request_params)
272
+ end
273
+ end
18
274
 
19
- module ClassMethods
20
- def middleware(&block)
21
- _connection(true, &block)
275
+ # Declares a new property by name
276
+ #
277
+ # @param name [Symbol] the name of the property
278
+ # @param options [Hash] property options
279
+ # @option options [Symbol] :type The property type
280
+ # @option options [Symbol] :default The default value for the property
281
+ def property(name, options = {})
282
+ schema.add(name, options)
22
283
  end
23
284
 
24
- def json_key
25
- self.name.demodulize.pluralize.underscore
285
+ # Declare multiple properties with the same optional options
286
+ #
287
+ # @param [Array<Symbol>] names
288
+ # @param options [Hash] property options
289
+ # @option options [Symbol] :type The property type
290
+ # @option options [Symbol] :default The default value for the property
291
+ def properties(*names)
292
+ options = names.last.is_a?(Hash) ? names.pop : {}
293
+ names.each do |name|
294
+ property name, options
295
+ end
26
296
  end
27
297
 
28
- def host
29
- @host || raise(NotImplementedError, 'host was not set')
298
+ def _belongs_to_associations
299
+ associations.select{|association| association.is_a?(Associations::BelongsTo::Association) }
30
300
  end
31
301
 
32
- def path
33
- json_key
302
+ def _prefix_path
303
+ paths = _belongs_to_associations.map do |a|
304
+ a.to_prefix_path(route_formatter)
305
+ end
306
+
307
+ paths.join("/")
308
+ end
309
+
310
+ def _set_prefix_path(attrs)
311
+ paths = _belongs_to_associations.map do |a|
312
+ a.set_prefix_path(attrs, route_formatter)
313
+ end
314
+
315
+ paths.join("/")
34
316
  end
35
317
 
36
- def ssl
37
- {}
318
+ def _new_scope
319
+ query_builder.new(self)
38
320
  end
39
321
 
40
- private
322
+ def _custom_headers=(headers)
323
+ _header_store.replace(headers)
324
+ end
41
325
 
42
- def human_attribute_name(attr, options = {})
43
- attr
326
+ def _header_store
327
+ Thread.current["json_api_client-#{resource_name}"] ||= {}
44
328
  end
45
329
 
46
- def lookup_ancestors
47
- [self]
330
+ def _build_connection(rebuild = false)
331
+ return connection_object unless connection_object.nil? || rebuild
332
+ self.connection_object = connection_class.new(connection_options.merge(site: site)).tap do |conn|
333
+ yield(conn) if block_given?
334
+ end
48
335
  end
49
336
  end
50
337
 
51
- def initialize(params={})
52
- (params || {}).slice(*association_names).each do |key, value|
53
- send(:"#{key}=", value)
338
+ # Instantiate a new resource object
339
+ #
340
+ # @param params [Hash] Attributes, links, and relationships
341
+ def initialize(params = {})
342
+ @persisted = nil
343
+ self.links = self.class.linker.new(params.delete("links") || {})
344
+ self.relationships = self.class.relationship_linker.new(self.class, params.delete("relationships") || {})
345
+ self.attributes = self.class.default_attributes.merge(params)
346
+
347
+ self.class.schema.each_property do |property|
348
+ attributes[property.name] = property.default unless attributes.has_key?(property.name) || property.default.nil?
54
349
  end
55
350
 
56
- self.attributes = params.except(*association_names) if params
57
- @errors = ActiveModel::Errors.new(self)
58
- super()
351
+ self.class.associations.each do |association|
352
+ if params.has_key?(association.attr_name.to_s)
353
+ set_attribute(association.attr_name, params[association.attr_name.to_s])
354
+ end
355
+ end
356
+ end
357
+
358
+ # Set the current attributes and try to save them
359
+ #
360
+ # @param attrs [Hash] Attributes to update
361
+ # @return [Boolean] Whether the update succeeded or not
362
+ def update_attributes(attrs = {})
363
+ self.attributes = attrs
364
+ save
365
+ end
366
+
367
+ # Alias to update_attributes
368
+ #
369
+ # @param attrs [Hash] Attributes to update
370
+ # @return [Boolean] Whether the update succeeded or not
371
+ def update(attrs = {})
372
+ update_attributes(attrs)
373
+ end
374
+
375
+ # Mark the record as persisted
376
+ def mark_as_persisted!
377
+ @persisted = true
378
+ end
379
+
380
+ # Whether or not this record has been persisted to the database previously
381
+ #
382
+ # @return [Boolean]
383
+ def persisted?
384
+ !!@persisted && has_attribute?(self.class.primary_key)
385
+ end
386
+
387
+ # Returns true if this is a new record (never persisted to the database)
388
+ #
389
+ # @return [Boolean]
390
+ def new_record?
391
+ !persisted?
59
392
  end
60
393
 
61
- # Returns an Enumerable of all key attributes if any is set, regardless
62
- # if the object is persisted or not.
63
- # Returns nil if there are no key attributes.
394
+ # When we represent this resource as a relationship, we do so with id & type
64
395
  #
65
- # (see ActiveModel::Conversion#to_key)
66
- def to_key
67
- to_param ? [to_param] : nil
396
+ # @return [Hash] Representation of this object as a relation
397
+ def as_relation
398
+ attributes.slice(:type, self.class.primary_key)
399
+ end
400
+
401
+ # When we represent this resource for serialization (create/update), we do so
402
+ # with this implementation
403
+ #
404
+ # @return [Hash] Representation of this object as JSONAPI object
405
+ def as_json_api(*)
406
+ attributes.slice(:id, :type).tap do |h|
407
+ relationships_for_serialization.tap do |r|
408
+ h[:relationships] = self.class.key_formatter.format_keys(r) unless r.empty?
409
+ end
410
+ h[:attributes] = self.class.key_formatter.format_keys(attributes_for_serialization)
411
+ end
412
+ end
413
+
414
+ def as_json(*)
415
+ attributes.slice(:id, :type).tap do |h|
416
+ relationships.as_json.tap do |r|
417
+ h[:relationships] = r unless r.empty?
418
+ end
419
+ h[:attributes] = attributes.except(:id, :type).as_json
420
+ end
421
+ end
422
+
423
+ # Mark all attributes for this record as dirty
424
+ def set_all_dirty!
425
+ set_all_attributes_dirty
426
+ relationships.set_all_attributes_dirty if relationships
68
427
  end
69
428
 
70
- private
429
+ def valid?(context = nil)
430
+ context ||= (new_record? ? :create : :update)
431
+ super(context)
432
+ end
433
+
434
+ # Commit the current changes to the resource to the remote server.
435
+ # If the resource was previously loaded from the server, we will
436
+ # try to update the record. Otherwise if it's a new record, then
437
+ # we will try to create it
438
+ #
439
+ # @return [Boolean] Whether or not the save succeeded
440
+ def save
441
+ return false unless valid?
442
+
443
+ self.last_result_set = if persisted?
444
+ self.class.requestor.update(self)
445
+ else
446
+ self.class.requestor.create(self)
447
+ end
71
448
 
72
- def read_attribute_for_validation(attr)
73
- read_attribute(attr)
449
+ if last_result_set.has_errors?
450
+ fill_errors
451
+ false
452
+ else
453
+ self.errors.clear if self.errors
454
+ mark_as_persisted!
455
+ if updated = last_result_set.first
456
+ self.attributes = updated.attributes
457
+ self.links.attributes = updated.links.attributes
458
+ self.relationships.attributes = updated.relationships.attributes
459
+ clear_changes_information
460
+ end
461
+ true
462
+ end
74
463
  end
75
464
 
76
- def method_missing(method, *args, &block)
77
- if respond_to_without_attributes?(method, true)
78
- super
465
+ # Try to destroy this resource
466
+ #
467
+ # @return [Boolean] Whether or not the destroy succeeded
468
+ def destroy
469
+ self.last_result_set = self.class.requestor.destroy(self)
470
+ if last_result_set.has_errors?
471
+ fill_errors
472
+ false
79
473
  else
80
- if method.to_s =~ /^(.*)=$/
81
- set_attribute($1, args.first)
82
- elsif has_attribute?(method)
83
- read_attribute(method)
84
- elsif has_association?(method)
85
- read_assocation(method)
474
+ self.attributes.clear
475
+ true
476
+ end
477
+ end
478
+
479
+ def inspect
480
+ "#<#{self.class.name}:@attributes=#{attributes.inspect}>"
481
+ end
482
+
483
+ protected
484
+
485
+ def method_missing(method, *args)
486
+ association = association_for(method)
487
+
488
+ return super unless association || (relationships && relationships.has_attribute?(method))
489
+
490
+ return nil unless relationship_definitions = relationships[method]
491
+
492
+ # look in included data
493
+ if relationship_definitions.key?("data")
494
+ # included.data_for returns an array, if the association is a has_one, then pick the first, otherise return the whole array
495
+ if association.is_a?(JSONAPI::Consumer::Associations::HasOne::Association)
496
+ return last_result_set.included.data_for(method, relationship_definitions).try(:first)
86
497
  else
87
- super
498
+ return last_result_set.included.data_for(method, relationship_definitions)
499
+ end
500
+ end
501
+
502
+ if association = association_for(method)
503
+ # look for a defined relationship url
504
+ if relationship_definitions["links"] && url = relationship_definitions["links"]["related"]
505
+ return association.data(url)
88
506
  end
89
507
  end
508
+ nil
509
+ end
510
+
511
+ def respond_to_missing?(symbol, include_all = false)
512
+ return true if relationships && relationships.has_attribute?(symbol)
513
+ return true if association_for(symbol)
514
+ super
515
+ end
516
+
517
+ def set_attribute(name, value)
518
+ property = property_for(name)
519
+ value = property.cast(value) if property
520
+ super(name, value)
521
+ end
522
+
523
+ def has_attribute?(attr_name)
524
+ !!property_for(attr_name) || super
525
+ end
526
+
527
+ def property_for(name)
528
+ self.class.schema.find(name)
529
+ end
530
+
531
+ def association_for(name)
532
+ self.class.associations.detect do |association|
533
+ association.attr_name.to_s == self.class.key_formatter.unformat(name)
534
+ end
535
+ end
536
+
537
+ def attributes_for_serialization
538
+ attributes.except(*self.class.read_only_attributes).slice(*changed)
539
+ end
540
+
541
+ def relationships_for_serialization
542
+ relationships.as_json_api
543
+ end
544
+
545
+ def fill_errors
546
+ last_result_set.errors.each do |error|
547
+ key = self.class.key_formatter.unformat(error.error_key)
548
+ errors.add(key, error.error_msg)
549
+ end
90
550
  end
91
551
  end
92
552
  end
@@ -0,0 +1,25 @@
1
+ require 'forwardable'
2
+
3
+ module JSONAPI::Consumer
4
+ class ResultSet < Array
5
+ extend Forwardable
6
+
7
+ attr_accessor :errors,
8
+ :record_class,
9
+ :meta,
10
+ :pages,
11
+ :uri,
12
+ :links,
13
+ :implementation,
14
+ :relationships,
15
+ :included
16
+
17
+ # pagination methods are handled by the paginator
18
+ def_delegators :pages, :total_pages, :total_entries, :total_count, :offset, :per_page, :current_page, :limit_value, :next_page, :previous_page, :out_of_bounds?
19
+
20
+ def has_errors?
21
+ errors.present?
22
+ end
23
+
24
+ end
25
+ end