json_api_client 1.23.0 → 1.24.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 (47) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +20 -20
  3. data/README.md +723 -705
  4. data/Rakefile +32 -32
  5. data/lib/json_api_client/associations/base_association.rb +33 -33
  6. data/lib/json_api_client/associations/belongs_to.rb +31 -31
  7. data/lib/json_api_client/associations/has_many.rb +7 -7
  8. data/lib/json_api_client/associations/has_one.rb +16 -16
  9. data/lib/json_api_client/associations.rb +7 -7
  10. data/lib/json_api_client/connection.rb +41 -41
  11. data/lib/json_api_client/error_collector.rb +91 -91
  12. data/lib/json_api_client/errors.rb +125 -125
  13. data/lib/json_api_client/formatter.rb +145 -145
  14. data/lib/json_api_client/helpers/associatable.rb +88 -88
  15. data/lib/json_api_client/helpers/callbacks.rb +27 -27
  16. data/lib/json_api_client/helpers/dirty.rb +75 -75
  17. data/lib/json_api_client/helpers/dynamic_attributes.rb +78 -78
  18. data/lib/json_api_client/helpers/uri.rb +9 -9
  19. data/lib/json_api_client/helpers.rb +9 -9
  20. data/lib/json_api_client/implementation.rb +11 -11
  21. data/lib/json_api_client/included_data.rb +58 -58
  22. data/lib/json_api_client/linking/links.rb +21 -21
  23. data/lib/json_api_client/linking/top_level_links.rb +39 -39
  24. data/lib/json_api_client/linking.rb +5 -5
  25. data/lib/json_api_client/meta_data.rb +19 -19
  26. data/lib/json_api_client/middleware/json_request.rb +26 -26
  27. data/lib/json_api_client/middleware/status.rb +67 -67
  28. data/lib/json_api_client/middleware.rb +6 -6
  29. data/lib/json_api_client/paginating/nested_param_paginator.rb +140 -140
  30. data/lib/json_api_client/paginating/paginator.rb +89 -89
  31. data/lib/json_api_client/paginating.rb +6 -6
  32. data/lib/json_api_client/parsers/parser.rb +102 -102
  33. data/lib/json_api_client/parsers.rb +4 -4
  34. data/lib/json_api_client/query/builder.rb +239 -239
  35. data/lib/json_api_client/query/requestor.rb +73 -73
  36. data/lib/json_api_client/query.rb +5 -5
  37. data/lib/json_api_client/relationships/relations.rb +55 -55
  38. data/lib/json_api_client/relationships/top_level_relations.rb +30 -30
  39. data/lib/json_api_client/relationships.rb +5 -5
  40. data/lib/json_api_client/request_params.rb +57 -57
  41. data/lib/json_api_client/resource.rb +671 -671
  42. data/lib/json_api_client/result_set.rb +25 -25
  43. data/lib/json_api_client/schema.rb +154 -154
  44. data/lib/json_api_client/utils.rb +53 -53
  45. data/lib/json_api_client/version.rb +3 -3
  46. data/lib/json_api_client.rb +30 -30
  47. metadata +55 -30
@@ -1,671 +1,671 @@
1
- require 'forwardable'
2
- require 'active_support/all'
3
- require 'active_model'
4
-
5
- module JsonApiClient
6
- class Resource
7
- extend ActiveModel::Naming
8
- extend ActiveModel::Translation
9
- include ActiveModel::Validations
10
- include ActiveModel::Conversion
11
- include ActiveModel::Serialization
12
-
13
- include Helpers::DynamicAttributes
14
- include Helpers::Dirty
15
- include Helpers::Associatable
16
-
17
- attr_accessor :last_result_set,
18
- :links,
19
- :relationships,
20
- :request_params
21
- class_attribute :site,
22
- :primary_key,
23
- :parser,
24
- :paginator,
25
- :connection_class,
26
- :connection_object,
27
- :connection_options,
28
- :query_builder,
29
- :linker,
30
- :relationship_linker,
31
- :read_only_attributes,
32
- :requestor_class,
33
- :json_key_format,
34
- :route_format,
35
- :request_params_class,
36
- :keep_request_params,
37
- :search_included_in_result_set,
38
- :custom_type_to_class,
39
- :raise_on_blank_find_param,
40
- instance_accessor: false
41
- class_attribute :add_defaults_to_changes,
42
- instance_writer: false
43
-
44
- class_attribute :_immutable,
45
- instance_writer: false,
46
- default: false
47
-
48
- self.primary_key = :id
49
- self.parser = Parsers::Parser
50
- self.paginator = Paginating::Paginator
51
- self.connection_class = Connection
52
- self.connection_options = {}
53
- self.query_builder = Query::Builder
54
- self.linker = Linking::Links
55
- self.relationship_linker = Relationships::Relations
56
- self.read_only_attributes = [:id, :type, :links, :meta, :relationships]
57
- self.requestor_class = Query::Requestor
58
- self.request_params_class = RequestParams
59
- self.keep_request_params = false
60
- self.add_defaults_to_changes = false
61
- self.search_included_in_result_set = false
62
- self.custom_type_to_class = {}
63
- self.raise_on_blank_find_param = false
64
-
65
- #:underscored_key, :camelized_key, :dasherized_key, or custom
66
- self.json_key_format = :underscored_key
67
-
68
- #:underscored_route, :camelized_route, :dasherized_route, or custom
69
- self.route_format = :underscored_route
70
-
71
- class << self
72
- extend Forwardable
73
- def_delegators :_new_scope, :where, :order, :includes, :select, :all, :paginate, :page, :with_params, :first, :find, :last
74
-
75
- def resolve_custom_type(type_name, class_name)
76
- classified_type = key_formatter.unformat(type_name.to_s).singularize.classify
77
- self.custom_type_to_class = custom_type_to_class.merge(classified_type => class_name.to_s)
78
- end
79
-
80
- # The table name for this resource. i.e. Article -> articles, Person -> people
81
- #
82
- # @return [String] The table name for this resource
83
- def table_name
84
- route_formatter.format(resource_name.pluralize)
85
- end
86
-
87
- # The name of a single resource. i.e. Article -> article, Person -> person
88
- #
89
- # @return [String]
90
- def resource_name
91
- name.demodulize.underscore
92
- end
93
-
94
- # Specifies the JSON API resource type. By default this is inferred
95
- # from the resource class name.
96
- #
97
- # @return [String] Resource path
98
- def type
99
- table_name
100
- end
101
-
102
- # Indicates whether this resource is mutable or immutable;
103
- # by default, all resources are mutable.
104
- #
105
- # @return [Boolean]
106
- def immutable(flag = true)
107
- self._immutable = flag
108
- end
109
-
110
- def inherited(subclass)
111
- subclass._immutable = false
112
- super
113
- end
114
-
115
- # Specifies the relative path that should be used for this resource;
116
- # by default, this is inferred from the resource class name.
117
- #
118
- # @return [String] Resource path
119
- def resource_path
120
- table_name
121
- end
122
-
123
- # Load a resource object from attributes and consider it persisted
124
- #
125
- # @return [Resource] Persisted resource object
126
- def load(params)
127
- new(params).tap do |resource|
128
- resource.mark_as_persisted!
129
- resource.clear_changes_information
130
- resource.relationships.clear_changes_information
131
- end
132
- end
133
-
134
- # Return/build a connection object
135
- #
136
- # @return [Connection] The connection to the json api server
137
- def connection(rebuild = false, &block)
138
- _build_connection(rebuild, &block)
139
- connection_object
140
- end
141
-
142
- # Param names that will be considered path params. They will be used
143
- # to build the resource path rather than treated as attributes
144
- #
145
- # @return [Array] Param name symbols of parameters that will be treated as path parameters
146
- def prefix_params
147
- _belongs_to_associations.map(&:param)
148
- end
149
-
150
- # Return the path or path pattern for this resource
151
- def path(params = nil)
152
- parts = [resource_path]
153
- if params && _prefix_path.present?
154
- path_params = params.delete(:path) || params
155
- parts.unshift(_set_prefix_path(path_params.symbolize_keys))
156
- else
157
- parts.unshift(_prefix_path)
158
- end
159
- parts.reject!(&:blank?)
160
- File.join(*parts)
161
- rescue KeyError
162
- raise ArgumentError, "Not all prefix parameters specified"
163
- end
164
-
165
- # Create a new instance of this resource class
166
- #
167
- # @param attributes [Hash] The attributes to create this resource with
168
- # @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.
169
- def create(attributes = {})
170
- new(attributes).tap do |resource|
171
- resource.save
172
- end
173
- end
174
-
175
- def create!(attributes = {})
176
- new(attributes).tap do |resource|
177
- raise(Errors::RecordNotSaved.new("Failed to save the record", resource)) unless resource.save
178
- end
179
- end
180
-
181
- # Within the given block, add these headers to all requests made by
182
- # the resource class
183
- #
184
- # @param headers [Hash] The headers to send along
185
- # @param block [Block] The block where headers will be set for
186
- def with_headers(headers)
187
- self._custom_headers = headers
188
- yield
189
- ensure
190
- self._custom_headers = {}
191
- end
192
-
193
- # The current custom headers to send with any request made by this
194
- # resource class
195
- #
196
- # @return [Hash] Headers
197
- def custom_headers
198
- return _header_store.to_h if superclass == Object
199
-
200
- superclass.custom_headers.merge(_header_store.to_h)
201
- end
202
-
203
- # Returns the requestor for this resource class
204
- #
205
- # @return [Requestor] The requestor for this resource class
206
- def requestor
207
- @requestor ||= requestor_class.new(self)
208
- end
209
-
210
- # Default attributes that every instance of this resource should be
211
- # initialized with. Optionally, override this method in a subclass.
212
- #
213
- # @return [Hash] Default attributes
214
- def default_attributes
215
- {type: type}
216
- end
217
-
218
- # Returns the schema for this resource class
219
- #
220
- # @return [Schema] The schema for this resource class
221
- def schema
222
- @schema ||= Schema.new
223
- end
224
-
225
- def key_formatter
226
- JsonApiClient::Formatter.formatter_for(json_key_format)
227
- end
228
-
229
- def route_formatter
230
- JsonApiClient::Formatter.formatter_for(route_format)
231
- end
232
-
233
- protected
234
-
235
- # Declares a new class/instance method that acts on the collection/member
236
- #
237
- # @param name [Symbol] the name of the endpoint
238
- # @param options [Hash] endpoint options
239
- # @option [Symbol] :on One of [:collection or :member] to decide whether it's a collect or member method
240
- # @option [Symbol] :request_method The request method (:get, :post, etc)
241
- def custom_endpoint(name, options = {})
242
- if _immutable
243
- request_method = options.fetch(:request_method, :get).to_sym
244
- raise JsonApiClient::Errors::ResourceImmutableError if request_method != :get
245
- end
246
-
247
- if :collection == options.delete(:on)
248
- collection_endpoint(name, options)
249
- else
250
- member_endpoint(name, options)
251
- end
252
- end
253
-
254
- # Declares a new class method that acts on the collection
255
- #
256
- # @param name [Symbol] the name of the endpoint and the method name
257
- # @param options [Hash] endpoint options
258
- # @option options [Symbol] :request_method The request method (:get, :post, etc)
259
- def collection_endpoint(name, options = {})
260
- metaclass = class << self
261
- self
262
- end
263
- endpoint_name = _name_for_route_format(name)
264
- metaclass.instance_eval do
265
- define_method(name) do |*params|
266
- request_params = params.first || {}
267
- requestor.custom(endpoint_name, options, request_params)
268
- end
269
- end
270
- end
271
-
272
- # Declares a new instance method that acts on the member object
273
- #
274
- # @param name [Symbol] the name of the endpoint and the method name
275
- # @param options [Hash] endpoint options
276
- # @option options [Symbol] :request_method The request method (:get, :post, etc)
277
- def member_endpoint(name, options = {})
278
- endpoint_name = self._name_for_route_format(name)
279
- define_method name do |*params|
280
- request_params = params.first || {}
281
- request_params[self.class.primary_key] = attributes.fetch(self.class.primary_key)
282
- self.class.requestor.custom(endpoint_name, options, request_params)
283
- end
284
- end
285
-
286
- # Declares a new property by name
287
- #
288
- # @param name [Symbol] the name of the property
289
- # @param options [Hash] property options
290
- # @option options [Symbol] :type The property type
291
- # @option options [Symbol] :default The default value for the property
292
- def property(name, options = {})
293
- schema.add(name, options)
294
- define_method(name) do
295
- attributes[name]
296
- end
297
- define_method("#{name}=") do |value|
298
- set_attribute(name, value)
299
- end
300
- end
301
-
302
- # Declare multiple properties with the same optional options
303
- #
304
- # @param [Array<Symbol>] names
305
- # @param options [Hash] property options
306
- # @option options [Symbol] :type The property type
307
- # @option options [Symbol] :default The default value for the property
308
- def properties(*names)
309
- options = names.last.is_a?(Hash) ? names.pop : {}
310
- names.each do |name|
311
- property name, options
312
- end
313
- end
314
-
315
- def _belongs_to_associations
316
- associations.select{|association| association.is_a?(Associations::BelongsTo::Association) }
317
- end
318
-
319
- def _prefix_path
320
- paths = _belongs_to_associations.map do |a|
321
- a.to_prefix_path(route_formatter)
322
- end
323
-
324
- paths.join("/")
325
- end
326
-
327
- def _set_prefix_path(attrs)
328
- paths = _belongs_to_associations.map do |a|
329
- a.set_prefix_path(attrs, route_formatter)
330
- end
331
-
332
- paths.join("/")
333
- end
334
-
335
- def _new_scope
336
- query_builder.new(self)
337
- end
338
-
339
- def _custom_headers=(headers)
340
- _header_store.replace(headers)
341
- end
342
-
343
- def _header_store
344
- Thread.current["json_api_client-#{resource_name}"] ||= {}
345
- end
346
-
347
- def _build_connection(rebuild = false)
348
- return connection_object unless connection_object.nil? || rebuild
349
- self.connection_object = connection_class.new(connection_options.merge(site: site)).tap do |conn|
350
- yield(conn) if block_given?
351
- end
352
- end
353
-
354
- def _name_for_route_format(name)
355
- case self.route_format
356
- when :dasherized_route
357
- name.to_s.dasherize
358
- when :camelized_route
359
- name.to_s.camelize(:lower)
360
- else
361
- name
362
- end
363
- end
364
- end
365
-
366
- # Instantiate a new resource object
367
- #
368
- # @param params [Hash] Attributes, links, and relationships
369
- def initialize(params = {})
370
- params = params.with_indifferent_access
371
- @persisted = nil
372
- @destroyed = nil
373
- self.links = self.class.linker.new(params.delete(:links) || {})
374
- self.relationships = self.class.relationship_linker.new(self.class, params.delete(:relationships) || {})
375
- self.attributes = self.class.default_attributes.merge params.except(*self.class.prefix_params)
376
- self.forget_change!(:type)
377
- self.__belongs_to_params = params.slice(*self.class.prefix_params)
378
-
379
- setup_default_properties
380
-
381
- self.class.associations.each do |association|
382
- if params.has_key?(association.attr_name.to_s)
383
- set_attribute(association.attr_name, params[association.attr_name.to_s])
384
- end
385
- end
386
- self.request_params = self.class.request_params_class.new(self.class)
387
- end
388
-
389
- # Set the current attributes and try to save them
390
- #
391
- # @param attrs [Hash] Attributes to update
392
- # @return [Boolean] Whether the update succeeded or not
393
- def update_attributes(attrs = {})
394
- self.attributes = attrs
395
- save
396
- end
397
-
398
- def update_attributes!(attrs = {})
399
- self.attributes = attrs
400
- save ? true : raise(Errors::RecordNotSaved.new("Failed to update the record", self))
401
- end
402
-
403
- # Alias to update_attributes
404
- #
405
- # @param attrs [Hash] Attributes to update
406
- # @return [Boolean] Whether the update succeeded or not
407
- def update(attrs = {})
408
- update_attributes(attrs)
409
- end
410
-
411
- def update!(attrs = {})
412
- update_attributes!(attrs)
413
- end
414
-
415
- # Mark the record as persisted
416
- def mark_as_persisted!
417
- @persisted = true
418
- end
419
-
420
- # Whether or not this record has been persisted to the database previously
421
- #
422
- # @return [Boolean]
423
- def persisted?
424
- !!@persisted && !destroyed? && has_attribute?(self.class.primary_key)
425
- end
426
-
427
- # Mark the record as destroyed
428
- def mark_as_destroyed!
429
- @destroyed = true
430
- end
431
-
432
- # Whether or not this record has been destroyed to the database previously
433
- #
434
- # @return [Boolean]
435
- def destroyed?
436
- !!@destroyed
437
- end
438
-
439
- # Returns true if this is a new record (never persisted to the database)
440
- #
441
- # @return [Boolean]
442
- def new_record?
443
- !persisted? && !destroyed?
444
- end
445
-
446
- # When we represent this resource as a relationship, we do so with id & type
447
- #
448
- # @return [Hash] Representation of this object as a relation
449
- def as_relation
450
- attributes.slice(:type, self.class.primary_key)
451
- end
452
-
453
- # When we represent this resource for serialization (create/update), we do so
454
- # with this implementation
455
- #
456
- # @return [Hash] Representation of this object as JSONAPI object
457
- def as_json_api(*)
458
- attributes.slice(self.class.primary_key, :type).tap do |h|
459
- relationships_for_serialization.tap do |r|
460
- h[:relationships] = self.class.key_formatter.format_keys(r) unless r.empty?
461
- end
462
- h[:attributes] = self.class.key_formatter.format_keys(attributes_for_serialization)
463
- end
464
- end
465
-
466
- def as_json(*)
467
- attributes.slice(self.class.primary_key, :type).tap do |h|
468
- relationships.as_json.tap do |r|
469
- h[:relationships] = r unless r.empty?
470
- end
471
- h[:attributes] = attributes.except(self.class.primary_key, :type).as_json
472
- end
473
- end
474
-
475
- # Mark all attributes for this record as dirty
476
- def set_all_dirty!
477
- set_all_attributes_dirty
478
- relationships.set_all_attributes_dirty if relationships
479
- end
480
-
481
- def valid?(context = nil)
482
- context ||= (new_record? ? :create : :update)
483
- super(context)
484
- end
485
-
486
- # Commit the current changes to the resource to the remote server.
487
- # If the resource was previously loaded from the server, we will
488
- # try to update the record. Otherwise if it's a new record, then
489
- # we will try to create it
490
- #
491
- # @return [Boolean] Whether or not the save succeeded
492
- def save
493
- return false unless valid?
494
- raise JsonApiClient::Errors::ResourceImmutableError if _immutable
495
-
496
- self.last_result_set = if persisted?
497
- self.class.requestor.update(self)
498
- else
499
- self.class.requestor.create(self)
500
- end
501
-
502
- if last_result_set.has_errors?
503
- fill_errors
504
- false
505
- else
506
- self.errors.clear if self.errors
507
- self.request_params.clear unless self.class.keep_request_params
508
- mark_as_persisted!
509
- if updated = last_result_set.first
510
- self.attributes = updated.attributes
511
- self.links.attributes = updated.links.attributes
512
- self.relationships.attributes = updated.relationships.attributes
513
- clear_changes_information
514
- self.relationships.clear_changes_information
515
- _clear_cached_relationships
516
- end
517
- true
518
- end
519
- end
520
-
521
- # Try to destroy this resource
522
- #
523
- # @return [Boolean] Whether or not the destroy succeeded
524
- def destroy
525
- raise JsonApiClient::Errors::ResourceImmutableError if _immutable
526
-
527
- self.last_result_set = self.class.requestor.destroy(self)
528
- if last_result_set.has_errors?
529
- fill_errors
530
- false
531
- else
532
- mark_as_destroyed!
533
- _clear_cached_relationships
534
- _clear_belongs_to_params
535
- true
536
- end
537
- end
538
-
539
- def inspect
540
- "#<#{self.class.name}:@attributes=#{attributes.inspect}>"
541
- end
542
-
543
- def request_includes(*includes)
544
- self.request_params.add_includes(includes)
545
- self
546
- end
547
-
548
- def reset_request_includes!
549
- self.request_params.reset_includes!
550
- self
551
- end
552
-
553
- def request_select(*fields)
554
- fields_by_type = fields.extract_options!
555
- fields_by_type[type.to_sym] = fields if fields.any?
556
- fields_by_type.each do |field_type, field_names|
557
- self.request_params.set_fields(field_type, field_names)
558
- end
559
- self
560
- end
561
-
562
- def reset_request_select!(*resource_types)
563
- resource_types = self.request_params.field_types if resource_types.empty?
564
- resource_types.each { |resource_type| self.request_params.remove_fields(resource_type) }
565
- self
566
- end
567
-
568
- def path_attributes
569
- _belongs_to_params.merge attributes.slice( self.class.primary_key ).with_indifferent_access
570
- end
571
-
572
- protected
573
-
574
- def setup_default_properties
575
- self.class.schema.each_property do |property|
576
- unless attributes.has_key?(property.name) || property.default.nil?
577
- attribute_will_change!(property.name) if add_defaults_to_changes
578
- attributes[property.name] = property.default
579
- end
580
- end
581
- end
582
-
583
- def relationship_definition_for(name)
584
- relationships[name] if relationships && relationships.has_attribute?(name)
585
- end
586
-
587
- def included_data_for(name, relationship_definition)
588
- last_result_set.included.data_for(name, relationship_definition)
589
- end
590
-
591
- def relationship_data_for(name, relationship_definition)
592
- # look in included data
593
- if relationship_definition.key?("data")
594
- if relationships.attribute_changed?(name)
595
- return relation_objects_for(name, relationship_definition)
596
- else
597
- return included_data_for(name, relationship_definition)
598
- end
599
- end
600
-
601
- return unless links = relationship_definition["links"]
602
- return unless url = links["related"]
603
-
604
- association_for(name).data(url)
605
- end
606
-
607
- def relation_objects_for(name, relationship_definition)
608
- data = relationship_definition["data"]
609
- assoc = association_for(name)
610
- return if data.nil? || assoc.nil?
611
- assoc.load_records(data)
612
- end
613
-
614
- def method_missing(method, *args)
615
- relationship_definition = relationship_definition_for(method)
616
-
617
- return super unless relationship_definition
618
-
619
- relationship_data_for(method, relationship_definition)
620
- end
621
-
622
- def respond_to_missing?(symbol, include_all = false)
623
- return true if relationships && relationships.has_attribute?(symbol)
624
- return true if association_for(symbol)
625
- super
626
- end
627
-
628
- def set_attribute(name, value)
629
- property = property_for(name)
630
- value = property.cast(value) if property
631
- super(name, value)
632
- end
633
-
634
- def has_attribute?(attr_name)
635
- !!property_for(attr_name) || super
636
- end
637
-
638
- def property_for(name)
639
- self.class.schema.find(name)
640
- end
641
-
642
- def association_for(name)
643
- self.class.associations.detect do |association|
644
- association.attr_name.to_s == self.class.key_formatter.unformat(name)
645
- end
646
- end
647
-
648
- def non_serializing_attributes
649
- self.class.read_only_attributes
650
- end
651
-
652
- def attributes_for_serialization
653
- attributes.except(*non_serializing_attributes).slice(*changed)
654
- end
655
-
656
- def relationships_for_serialization
657
- relationships.as_json_api
658
- end
659
-
660
- def error_message_for(error)
661
- error.error_msg
662
- end
663
-
664
- def fill_errors
665
- last_result_set.errors.each do |error|
666
- key = self.class.key_formatter.unformat(error.error_key)
667
- errors.add(key, error_message_for(error))
668
- end
669
- end
670
- end
671
- end
1
+ require 'forwardable'
2
+ require 'active_support/all'
3
+ require 'active_model'
4
+
5
+ module JsonApiClient
6
+ class Resource
7
+ extend ActiveModel::Naming
8
+ extend ActiveModel::Translation
9
+ include ActiveModel::Validations
10
+ include ActiveModel::Conversion
11
+ include ActiveModel::Serialization
12
+
13
+ include Helpers::DynamicAttributes
14
+ include Helpers::Dirty
15
+ include Helpers::Associatable
16
+
17
+ attr_accessor :last_result_set,
18
+ :links,
19
+ :relationships,
20
+ :request_params
21
+ class_attribute :site,
22
+ :primary_key,
23
+ :parser,
24
+ :paginator,
25
+ :connection_class,
26
+ :connection_object,
27
+ :connection_options,
28
+ :query_builder,
29
+ :linker,
30
+ :relationship_linker,
31
+ :read_only_attributes,
32
+ :requestor_class,
33
+ :json_key_format,
34
+ :route_format,
35
+ :request_params_class,
36
+ :keep_request_params,
37
+ :search_included_in_result_set,
38
+ :custom_type_to_class,
39
+ :raise_on_blank_find_param,
40
+ instance_accessor: false
41
+ class_attribute :add_defaults_to_changes,
42
+ instance_writer: false
43
+
44
+ class_attribute :_immutable,
45
+ instance_writer: false,
46
+ default: false
47
+
48
+ self.primary_key = :id
49
+ self.parser = Parsers::Parser
50
+ self.paginator = Paginating::Paginator
51
+ self.connection_class = Connection
52
+ self.connection_options = {}
53
+ self.query_builder = Query::Builder
54
+ self.linker = Linking::Links
55
+ self.relationship_linker = Relationships::Relations
56
+ self.read_only_attributes = [:id, :type, :links, :meta, :relationships]
57
+ self.requestor_class = Query::Requestor
58
+ self.request_params_class = RequestParams
59
+ self.keep_request_params = false
60
+ self.add_defaults_to_changes = false
61
+ self.search_included_in_result_set = false
62
+ self.custom_type_to_class = {}
63
+ self.raise_on_blank_find_param = false
64
+
65
+ #:underscored_key, :camelized_key, :dasherized_key, or custom
66
+ self.json_key_format = :underscored_key
67
+
68
+ #:underscored_route, :camelized_route, :dasherized_route, or custom
69
+ self.route_format = :underscored_route
70
+
71
+ class << self
72
+ extend Forwardable
73
+ def_delegators :_new_scope, :where, :order, :includes, :select, :all, :paginate, :page, :with_params, :first, :find, :last
74
+
75
+ def resolve_custom_type(type_name, class_name)
76
+ classified_type = key_formatter.unformat(type_name.to_s).singularize.classify
77
+ self.custom_type_to_class = custom_type_to_class.merge(classified_type => class_name.to_s)
78
+ end
79
+
80
+ # The table name for this resource. i.e. Article -> articles, Person -> people
81
+ #
82
+ # @return [String] The table name for this resource
83
+ def table_name
84
+ route_formatter.format(resource_name.pluralize)
85
+ end
86
+
87
+ # The name of a single resource. i.e. Article -> article, Person -> person
88
+ #
89
+ # @return [String]
90
+ def resource_name
91
+ name.demodulize.underscore
92
+ end
93
+
94
+ # Specifies the JSON API resource type. By default this is inferred
95
+ # from the resource class name.
96
+ #
97
+ # @return [String] Resource path
98
+ def type
99
+ table_name
100
+ end
101
+
102
+ # Indicates whether this resource is mutable or immutable;
103
+ # by default, all resources are mutable.
104
+ #
105
+ # @return [Boolean]
106
+ def immutable(flag = true)
107
+ self._immutable = flag
108
+ end
109
+
110
+ def inherited(subclass)
111
+ subclass._immutable = false
112
+ super
113
+ end
114
+
115
+ # Specifies the relative path that should be used for this resource;
116
+ # by default, this is inferred from the resource class name.
117
+ #
118
+ # @return [String] Resource path
119
+ def resource_path
120
+ table_name
121
+ end
122
+
123
+ # Load a resource object from attributes and consider it persisted
124
+ #
125
+ # @return [Resource] Persisted resource object
126
+ def load(params)
127
+ new(params).tap do |resource|
128
+ resource.mark_as_persisted!
129
+ resource.clear_changes_information
130
+ resource.relationships.clear_changes_information
131
+ end
132
+ end
133
+
134
+ # Return/build a connection object
135
+ #
136
+ # @return [Connection] The connection to the json api server
137
+ def connection(rebuild = false, &block)
138
+ _build_connection(rebuild, &block)
139
+ connection_object
140
+ end
141
+
142
+ # Param names that will be considered path params. They will be used
143
+ # to build the resource path rather than treated as attributes
144
+ #
145
+ # @return [Array] Param name symbols of parameters that will be treated as path parameters
146
+ def prefix_params
147
+ _belongs_to_associations.map(&:param)
148
+ end
149
+
150
+ # Return the path or path pattern for this resource
151
+ def path(params = nil)
152
+ parts = [resource_path]
153
+ if params && _prefix_path.present?
154
+ path_params = params.delete(:path) || params
155
+ parts.unshift(_set_prefix_path(path_params.symbolize_keys))
156
+ else
157
+ parts.unshift(_prefix_path)
158
+ end
159
+ parts.reject!(&:blank?)
160
+ File.join(*parts)
161
+ rescue KeyError
162
+ raise ArgumentError, "Not all prefix parameters specified"
163
+ end
164
+
165
+ # Create a new instance of this resource class
166
+ #
167
+ # @param attributes [Hash] The attributes to create this resource with
168
+ # @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.
169
+ def create(attributes = {})
170
+ new(attributes).tap do |resource|
171
+ resource.save
172
+ end
173
+ end
174
+
175
+ def create!(attributes = {})
176
+ new(attributes).tap do |resource|
177
+ raise(Errors::RecordNotSaved.new("Failed to save the record", resource)) unless resource.save
178
+ end
179
+ end
180
+
181
+ # Within the given block, add these headers to all requests made by
182
+ # the resource class
183
+ #
184
+ # @param headers [Hash] The headers to send along
185
+ # @param block [Block] The block where headers will be set for
186
+ def with_headers(headers)
187
+ self._custom_headers = headers
188
+ yield
189
+ ensure
190
+ self._custom_headers = {}
191
+ end
192
+
193
+ # The current custom headers to send with any request made by this
194
+ # resource class
195
+ #
196
+ # @return [Hash] Headers
197
+ def custom_headers
198
+ return _header_store.to_h if superclass == Object
199
+
200
+ superclass.custom_headers.merge(_header_store.to_h)
201
+ end
202
+
203
+ # Returns the requestor for this resource class
204
+ #
205
+ # @return [Requestor] The requestor for this resource class
206
+ def requestor
207
+ @requestor ||= requestor_class.new(self)
208
+ end
209
+
210
+ # Default attributes that every instance of this resource should be
211
+ # initialized with. Optionally, override this method in a subclass.
212
+ #
213
+ # @return [Hash] Default attributes
214
+ def default_attributes
215
+ {type: type}
216
+ end
217
+
218
+ # Returns the schema for this resource class
219
+ #
220
+ # @return [Schema] The schema for this resource class
221
+ def schema
222
+ @schema ||= Schema.new
223
+ end
224
+
225
+ def key_formatter
226
+ JsonApiClient::Formatter.formatter_for(json_key_format)
227
+ end
228
+
229
+ def route_formatter
230
+ JsonApiClient::Formatter.formatter_for(route_format)
231
+ end
232
+
233
+ protected
234
+
235
+ # Declares a new class/instance method that acts on the collection/member
236
+ #
237
+ # @param name [Symbol] the name of the endpoint
238
+ # @param options [Hash] endpoint options
239
+ # @option [Symbol] :on One of [:collection or :member] to decide whether it's a collect or member method
240
+ # @option [Symbol] :request_method The request method (:get, :post, etc)
241
+ def custom_endpoint(name, options = {})
242
+ if _immutable
243
+ request_method = options.fetch(:request_method, :get).to_sym
244
+ raise JsonApiClient::Errors::ResourceImmutableError if request_method != :get
245
+ end
246
+
247
+ if :collection == options.delete(:on)
248
+ collection_endpoint(name, options)
249
+ else
250
+ member_endpoint(name, options)
251
+ end
252
+ end
253
+
254
+ # Declares a new class method that acts on the collection
255
+ #
256
+ # @param name [Symbol] the name of the endpoint and the method name
257
+ # @param options [Hash] endpoint options
258
+ # @option options [Symbol] :request_method The request method (:get, :post, etc)
259
+ def collection_endpoint(name, options = {})
260
+ metaclass = class << self
261
+ self
262
+ end
263
+ endpoint_name = _name_for_route_format(name)
264
+ metaclass.instance_eval do
265
+ define_method(name) do |*params|
266
+ request_params = params.first || {}
267
+ requestor.custom(endpoint_name, options, request_params)
268
+ end
269
+ end
270
+ end
271
+
272
+ # Declares a new instance method that acts on the member object
273
+ #
274
+ # @param name [Symbol] the name of the endpoint and the method name
275
+ # @param options [Hash] endpoint options
276
+ # @option options [Symbol] :request_method The request method (:get, :post, etc)
277
+ def member_endpoint(name, options = {})
278
+ endpoint_name = self._name_for_route_format(name)
279
+ define_method name do |*params|
280
+ request_params = params.first || {}
281
+ request_params[self.class.primary_key] = attributes.fetch(self.class.primary_key)
282
+ self.class.requestor.custom(endpoint_name, options, request_params)
283
+ end
284
+ end
285
+
286
+ # Declares a new property by name
287
+ #
288
+ # @param name [Symbol] the name of the property
289
+ # @param options [Hash] property options
290
+ # @option options [Symbol] :type The property type
291
+ # @option options [Symbol] :default The default value for the property
292
+ def property(name, options = {})
293
+ schema.add(name, options)
294
+ define_method(name) do
295
+ attributes[name]
296
+ end
297
+ define_method("#{name}=") do |value|
298
+ set_attribute(name, value)
299
+ end
300
+ end
301
+
302
+ # Declare multiple properties with the same optional options
303
+ #
304
+ # @param [Array<Symbol>] names
305
+ # @param options [Hash] property options
306
+ # @option options [Symbol] :type The property type
307
+ # @option options [Symbol] :default The default value for the property
308
+ def properties(*names)
309
+ options = names.last.is_a?(Hash) ? names.pop : {}
310
+ names.each do |name|
311
+ property name, options
312
+ end
313
+ end
314
+
315
+ def _belongs_to_associations
316
+ associations.select{|association| association.is_a?(Associations::BelongsTo::Association) }
317
+ end
318
+
319
+ def _prefix_path
320
+ paths = _belongs_to_associations.map do |a|
321
+ a.to_prefix_path(route_formatter)
322
+ end
323
+
324
+ paths.join("/")
325
+ end
326
+
327
+ def _set_prefix_path(attrs)
328
+ paths = _belongs_to_associations.map do |a|
329
+ a.set_prefix_path(attrs, route_formatter)
330
+ end
331
+
332
+ paths.join("/")
333
+ end
334
+
335
+ def _new_scope
336
+ query_builder.new(self)
337
+ end
338
+
339
+ def _custom_headers=(headers)
340
+ _header_store.replace(headers)
341
+ end
342
+
343
+ def _header_store
344
+ Thread.current["json_api_client-#{resource_name}"] ||= {}
345
+ end
346
+
347
+ def _build_connection(rebuild = false)
348
+ return connection_object unless connection_object.nil? || rebuild
349
+ self.connection_object = connection_class.new(connection_options.merge(site: site)).tap do |conn|
350
+ yield(conn) if block_given?
351
+ end
352
+ end
353
+
354
+ def _name_for_route_format(name)
355
+ case self.route_format
356
+ when :dasherized_route
357
+ name.to_s.dasherize
358
+ when :camelized_route
359
+ name.to_s.camelize(:lower)
360
+ else
361
+ name
362
+ end
363
+ end
364
+ end
365
+
366
+ # Instantiate a new resource object
367
+ #
368
+ # @param params [Hash] Attributes, links, and relationships
369
+ def initialize(params = {})
370
+ params = params.with_indifferent_access
371
+ @persisted = nil
372
+ @destroyed = nil
373
+ self.links = self.class.linker.new(params.delete(:links) || {})
374
+ self.relationships = self.class.relationship_linker.new(self.class, params.delete(:relationships) || {})
375
+ self.attributes = self.class.default_attributes.merge params.except(*self.class.prefix_params)
376
+ self.forget_change!(:type)
377
+ self.__belongs_to_params = params.slice(*self.class.prefix_params)
378
+
379
+ setup_default_properties
380
+
381
+ self.class.associations.each do |association|
382
+ if params.has_key?(association.attr_name.to_s)
383
+ set_attribute(association.attr_name, params[association.attr_name.to_s])
384
+ end
385
+ end
386
+ self.request_params = self.class.request_params_class.new(self.class)
387
+ end
388
+
389
+ # Set the current attributes and try to save them
390
+ #
391
+ # @param attrs [Hash] Attributes to update
392
+ # @return [Boolean] Whether the update succeeded or not
393
+ def update_attributes(attrs = {})
394
+ self.attributes = attrs
395
+ save
396
+ end
397
+
398
+ def update_attributes!(attrs = {})
399
+ self.attributes = attrs
400
+ save ? true : raise(Errors::RecordNotSaved.new("Failed to update the record", self))
401
+ end
402
+
403
+ # Alias to update_attributes
404
+ #
405
+ # @param attrs [Hash] Attributes to update
406
+ # @return [Boolean] Whether the update succeeded or not
407
+ def update(attrs = {})
408
+ update_attributes(attrs)
409
+ end
410
+
411
+ def update!(attrs = {})
412
+ update_attributes!(attrs)
413
+ end
414
+
415
+ # Mark the record as persisted
416
+ def mark_as_persisted!
417
+ @persisted = true
418
+ end
419
+
420
+ # Whether or not this record has been persisted to the database previously
421
+ #
422
+ # @return [Boolean]
423
+ def persisted?
424
+ !!@persisted && !destroyed? && has_attribute?(self.class.primary_key)
425
+ end
426
+
427
+ # Mark the record as destroyed
428
+ def mark_as_destroyed!
429
+ @destroyed = true
430
+ end
431
+
432
+ # Whether or not this record has been destroyed to the database previously
433
+ #
434
+ # @return [Boolean]
435
+ def destroyed?
436
+ !!@destroyed
437
+ end
438
+
439
+ # Returns true if this is a new record (never persisted to the database)
440
+ #
441
+ # @return [Boolean]
442
+ def new_record?
443
+ !persisted? && !destroyed?
444
+ end
445
+
446
+ # When we represent this resource as a relationship, we do so with id & type
447
+ #
448
+ # @return [Hash] Representation of this object as a relation
449
+ def as_relation
450
+ attributes.slice(:type, self.class.primary_key)
451
+ end
452
+
453
+ # When we represent this resource for serialization (create/update), we do so
454
+ # with this implementation
455
+ #
456
+ # @return [Hash] Representation of this object as JSONAPI object
457
+ def as_json_api(*)
458
+ attributes.slice(self.class.primary_key, :type).tap do |h|
459
+ relationships_for_serialization.tap do |r|
460
+ h[:relationships] = self.class.key_formatter.format_keys(r) unless r.empty?
461
+ end
462
+ h[:attributes] = self.class.key_formatter.format_keys(attributes_for_serialization)
463
+ end
464
+ end
465
+
466
+ def as_json(*)
467
+ attributes.slice(self.class.primary_key, :type).tap do |h|
468
+ relationships.as_json.tap do |r|
469
+ h[:relationships] = r unless r.empty?
470
+ end
471
+ h[:attributes] = attributes.except(self.class.primary_key, :type).as_json
472
+ end
473
+ end
474
+
475
+ # Mark all attributes for this record as dirty
476
+ def set_all_dirty!
477
+ set_all_attributes_dirty
478
+ relationships.set_all_attributes_dirty if relationships
479
+ end
480
+
481
+ def valid?(context = nil)
482
+ context ||= (new_record? ? :create : :update)
483
+ super(context)
484
+ end
485
+
486
+ # Commit the current changes to the resource to the remote server.
487
+ # If the resource was previously loaded from the server, we will
488
+ # try to update the record. Otherwise if it's a new record, then
489
+ # we will try to create it
490
+ #
491
+ # @return [Boolean] Whether or not the save succeeded
492
+ def save
493
+ return false unless valid?
494
+ raise JsonApiClient::Errors::ResourceImmutableError if _immutable
495
+
496
+ self.last_result_set = if persisted?
497
+ self.class.requestor.update(self)
498
+ else
499
+ self.class.requestor.create(self)
500
+ end
501
+
502
+ if last_result_set.has_errors?
503
+ fill_errors
504
+ false
505
+ else
506
+ self.errors.clear if self.errors
507
+ self.request_params.clear unless self.class.keep_request_params
508
+ mark_as_persisted!
509
+ if updated = last_result_set.first
510
+ self.attributes = updated.attributes
511
+ self.links.attributes = updated.links.attributes
512
+ self.relationships.attributes = updated.relationships.attributes
513
+ clear_changes_information
514
+ self.relationships.clear_changes_information
515
+ _clear_cached_relationships
516
+ end
517
+ true
518
+ end
519
+ end
520
+
521
+ # Try to destroy this resource
522
+ #
523
+ # @return [Boolean] Whether or not the destroy succeeded
524
+ def destroy
525
+ raise JsonApiClient::Errors::ResourceImmutableError if _immutable
526
+
527
+ self.last_result_set = self.class.requestor.destroy(self)
528
+ if last_result_set.has_errors?
529
+ fill_errors
530
+ false
531
+ else
532
+ mark_as_destroyed!
533
+ _clear_cached_relationships
534
+ _clear_belongs_to_params
535
+ true
536
+ end
537
+ end
538
+
539
+ def inspect
540
+ "#<#{self.class.name}:@attributes=#{attributes.inspect}>"
541
+ end
542
+
543
+ def request_includes(*includes)
544
+ self.request_params.add_includes(includes)
545
+ self
546
+ end
547
+
548
+ def reset_request_includes!
549
+ self.request_params.reset_includes!
550
+ self
551
+ end
552
+
553
+ def request_select(*fields)
554
+ fields_by_type = fields.extract_options!
555
+ fields_by_type[type.to_sym] = fields if fields.any?
556
+ fields_by_type.each do |field_type, field_names|
557
+ self.request_params.set_fields(field_type, field_names)
558
+ end
559
+ self
560
+ end
561
+
562
+ def reset_request_select!(*resource_types)
563
+ resource_types = self.request_params.field_types if resource_types.empty?
564
+ resource_types.each { |resource_type| self.request_params.remove_fields(resource_type) }
565
+ self
566
+ end
567
+
568
+ def path_attributes
569
+ _belongs_to_params.merge attributes.slice( self.class.primary_key ).with_indifferent_access
570
+ end
571
+
572
+ protected
573
+
574
+ def setup_default_properties
575
+ self.class.schema.each_property do |property|
576
+ unless attributes.has_key?(property.name) || property.default.nil?
577
+ attribute_will_change!(property.name) if add_defaults_to_changes
578
+ attributes[property.name] = property.default
579
+ end
580
+ end
581
+ end
582
+
583
+ def relationship_definition_for(name)
584
+ relationships[name] if relationships && relationships.has_attribute?(name)
585
+ end
586
+
587
+ def included_data_for(name, relationship_definition)
588
+ last_result_set.included.data_for(name, relationship_definition)
589
+ end
590
+
591
+ def relationship_data_for(name, relationship_definition)
592
+ # look in included data
593
+ if relationship_definition.key?("data")
594
+ if relationships.attribute_changed?(name)
595
+ return relation_objects_for(name, relationship_definition)
596
+ else
597
+ return included_data_for(name, relationship_definition)
598
+ end
599
+ end
600
+
601
+ return unless links = relationship_definition["links"]
602
+ return unless url = links["related"]
603
+
604
+ association_for(name).data(url)
605
+ end
606
+
607
+ def relation_objects_for(name, relationship_definition)
608
+ data = relationship_definition["data"]
609
+ assoc = association_for(name)
610
+ return if data.nil? || assoc.nil?
611
+ assoc.load_records(data)
612
+ end
613
+
614
+ def method_missing(method, *args)
615
+ relationship_definition = relationship_definition_for(method)
616
+
617
+ return super unless relationship_definition
618
+
619
+ relationship_data_for(method, relationship_definition)
620
+ end
621
+
622
+ def respond_to_missing?(symbol, include_all = false)
623
+ return true if relationships && relationships.has_attribute?(symbol)
624
+ return true if association_for(symbol)
625
+ super
626
+ end
627
+
628
+ def set_attribute(name, value)
629
+ property = property_for(name)
630
+ value = property.cast(value) if property
631
+ super(name, value)
632
+ end
633
+
634
+ def has_attribute?(attr_name)
635
+ !!property_for(attr_name) || super
636
+ end
637
+
638
+ def property_for(name)
639
+ self.class.schema.find(name)
640
+ end
641
+
642
+ def association_for(name)
643
+ self.class.associations.detect do |association|
644
+ association.attr_name.to_s == self.class.key_formatter.unformat(name)
645
+ end
646
+ end
647
+
648
+ def non_serializing_attributes
649
+ self.class.read_only_attributes
650
+ end
651
+
652
+ def attributes_for_serialization
653
+ attributes.except(*non_serializing_attributes).slice(*changed)
654
+ end
655
+
656
+ def relationships_for_serialization
657
+ relationships.as_json_api
658
+ end
659
+
660
+ def error_message_for(error)
661
+ error.error_msg
662
+ end
663
+
664
+ def fill_errors
665
+ last_result_set.errors.each do |error|
666
+ key = self.class.key_formatter.unformat(error.error_key)
667
+ errors.add(key, error_message_for(error))
668
+ end
669
+ end
670
+ end
671
+ end