jsonapi-consumer 0.1.1 → 1.0.0

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