carwow-json_api_client 1.19.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (48) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +20 -0
  3. data/README.md +706 -0
  4. data/Rakefile +32 -0
  5. data/lib/json_api_client.rb +30 -0
  6. data/lib/json_api_client/associations.rb +8 -0
  7. data/lib/json_api_client/associations/base_association.rb +33 -0
  8. data/lib/json_api_client/associations/belongs_to.rb +31 -0
  9. data/lib/json_api_client/associations/has_many.rb +8 -0
  10. data/lib/json_api_client/associations/has_one.rb +16 -0
  11. data/lib/json_api_client/connection.rb +41 -0
  12. data/lib/json_api_client/error_collector.rb +91 -0
  13. data/lib/json_api_client/errors.rb +107 -0
  14. data/lib/json_api_client/formatter.rb +145 -0
  15. data/lib/json_api_client/helpers.rb +9 -0
  16. data/lib/json_api_client/helpers/associatable.rb +88 -0
  17. data/lib/json_api_client/helpers/callbacks.rb +27 -0
  18. data/lib/json_api_client/helpers/dirty.rb +75 -0
  19. data/lib/json_api_client/helpers/dynamic_attributes.rb +78 -0
  20. data/lib/json_api_client/helpers/uri.rb +9 -0
  21. data/lib/json_api_client/implementation.rb +12 -0
  22. data/lib/json_api_client/included_data.rb +58 -0
  23. data/lib/json_api_client/linking.rb +6 -0
  24. data/lib/json_api_client/linking/links.rb +22 -0
  25. data/lib/json_api_client/linking/top_level_links.rb +39 -0
  26. data/lib/json_api_client/meta_data.rb +19 -0
  27. data/lib/json_api_client/middleware.rb +7 -0
  28. data/lib/json_api_client/middleware/json_request.rb +26 -0
  29. data/lib/json_api_client/middleware/parse_json.rb +31 -0
  30. data/lib/json_api_client/middleware/status.rb +67 -0
  31. data/lib/json_api_client/paginating.rb +6 -0
  32. data/lib/json_api_client/paginating/nested_param_paginator.rb +140 -0
  33. data/lib/json_api_client/paginating/paginator.rb +89 -0
  34. data/lib/json_api_client/parsers.rb +5 -0
  35. data/lib/json_api_client/parsers/parser.rb +102 -0
  36. data/lib/json_api_client/query.rb +6 -0
  37. data/lib/json_api_client/query/builder.rb +239 -0
  38. data/lib/json_api_client/query/requestor.rb +73 -0
  39. data/lib/json_api_client/relationships.rb +6 -0
  40. data/lib/json_api_client/relationships/relations.rb +55 -0
  41. data/lib/json_api_client/relationships/top_level_relations.rb +30 -0
  42. data/lib/json_api_client/request_params.rb +57 -0
  43. data/lib/json_api_client/resource.rb +643 -0
  44. data/lib/json_api_client/result_set.rb +25 -0
  45. data/lib/json_api_client/schema.rb +154 -0
  46. data/lib/json_api_client/utils.rb +48 -0
  47. data/lib/json_api_client/version.rb +3 -0
  48. metadata +213 -0
@@ -0,0 +1,643 @@
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
+ # Within the given block, add these headers to all requests made by
176
+ # the resource class
177
+ #
178
+ # @param headers [Hash] The headers to send along
179
+ # @param block [Block] The block where headers will be set for
180
+ def with_headers(headers)
181
+ self._custom_headers = headers
182
+ yield
183
+ ensure
184
+ self._custom_headers = {}
185
+ end
186
+
187
+ # The current custom headers to send with any request made by this
188
+ # resource class
189
+ #
190
+ # @return [Hash] Headers
191
+ def custom_headers
192
+ return _header_store.to_h if superclass == Object
193
+
194
+ superclass.custom_headers.merge(_header_store.to_h)
195
+ end
196
+
197
+ # Returns the requestor for this resource class
198
+ #
199
+ # @return [Requestor] The requestor for this resource class
200
+ def requestor
201
+ @requestor ||= requestor_class.new(self)
202
+ end
203
+
204
+ # Default attributes that every instance of this resource should be
205
+ # initialized with. Optionally, override this method in a subclass.
206
+ #
207
+ # @return [Hash] Default attributes
208
+ def default_attributes
209
+ {type: type}
210
+ end
211
+
212
+ # Returns the schema for this resource class
213
+ #
214
+ # @return [Schema] The schema for this resource class
215
+ def schema
216
+ @schema ||= Schema.new
217
+ end
218
+
219
+ def key_formatter
220
+ JsonApiClient::Formatter.formatter_for(json_key_format)
221
+ end
222
+
223
+ def route_formatter
224
+ JsonApiClient::Formatter.formatter_for(route_format)
225
+ end
226
+
227
+ protected
228
+
229
+ # Declares a new class/instance method that acts on the collection/member
230
+ #
231
+ # @param name [Symbol] the name of the endpoint
232
+ # @param options [Hash] endpoint options
233
+ # @option [Symbol] :on One of [:collection or :member] to decide whether it's a collect or member method
234
+ # @option [Symbol] :request_method The request method (:get, :post, etc)
235
+ def custom_endpoint(name, options = {})
236
+ if _immutable
237
+ request_method = options.fetch(:request_method, :get).to_sym
238
+ raise JsonApiClient::Errors::ResourceImmutableError if request_method != :get
239
+ end
240
+
241
+ if :collection == options.delete(:on)
242
+ collection_endpoint(name, options)
243
+ else
244
+ member_endpoint(name, options)
245
+ end
246
+ end
247
+
248
+ # Declares a new class method that acts on the collection
249
+ #
250
+ # @param name [Symbol] the name of the endpoint and the method name
251
+ # @param options [Hash] endpoint options
252
+ # @option options [Symbol] :request_method The request method (:get, :post, etc)
253
+ def collection_endpoint(name, options = {})
254
+ metaclass = class << self
255
+ self
256
+ end
257
+ metaclass.instance_eval do
258
+ define_method(name) do |*params|
259
+ request_params = params.first || {}
260
+ requestor.custom(name, options, request_params)
261
+ end
262
+ end
263
+ end
264
+
265
+ # Declares a new instance method that acts on the member object
266
+ #
267
+ # @param name [Symbol] the name of the endpoint and the method name
268
+ # @param options [Hash] endpoint options
269
+ # @option options [Symbol] :request_method The request method (:get, :post, etc)
270
+ def member_endpoint(name, options = {})
271
+ define_method name do |*params|
272
+ request_params = params.first || {}
273
+ request_params[self.class.primary_key] = attributes.fetch(self.class.primary_key)
274
+ self.class.requestor.custom(name, options, request_params)
275
+ end
276
+ end
277
+
278
+ # Declares a new property by name
279
+ #
280
+ # @param name [Symbol] the name of the property
281
+ # @param options [Hash] property options
282
+ # @option options [Symbol] :type The property type
283
+ # @option options [Symbol] :default The default value for the property
284
+ def property(name, options = {})
285
+ schema.add(name, options)
286
+ define_method(name) do
287
+ attributes[name]
288
+ end
289
+ define_method("#{name}=") do |value|
290
+ set_attribute(name, value)
291
+ end
292
+ end
293
+
294
+ # Declare multiple properties with the same optional options
295
+ #
296
+ # @param [Array<Symbol>] names
297
+ # @param options [Hash] property options
298
+ # @option options [Symbol] :type The property type
299
+ # @option options [Symbol] :default The default value for the property
300
+ def properties(*names)
301
+ options = names.last.is_a?(Hash) ? names.pop : {}
302
+ names.each do |name|
303
+ property name, options
304
+ end
305
+ end
306
+
307
+ def _belongs_to_associations
308
+ associations.select{|association| association.is_a?(Associations::BelongsTo::Association) }
309
+ end
310
+
311
+ def _prefix_path
312
+ paths = _belongs_to_associations.map do |a|
313
+ a.to_prefix_path(route_formatter)
314
+ end
315
+
316
+ paths.compact.join("/")
317
+ end
318
+
319
+ def _set_prefix_path(attrs)
320
+ paths = _belongs_to_associations.map do |a|
321
+ a.set_prefix_path(attrs, route_formatter)
322
+ end
323
+
324
+ paths.compact.join("/")
325
+ end
326
+
327
+ def _new_scope
328
+ query_builder.new(self)
329
+ end
330
+
331
+ def _custom_headers=(headers)
332
+ _header_store.replace(headers)
333
+ end
334
+
335
+ def _header_store
336
+ Thread.current["json_api_client-#{resource_name}"] ||= {}
337
+ end
338
+
339
+ def _build_connection(rebuild = false)
340
+ return connection_object unless connection_object.nil? || rebuild
341
+ self.connection_object = connection_class.new(connection_options.merge(site: site)).tap do |conn|
342
+ yield(conn) if block_given?
343
+ end
344
+ end
345
+ end
346
+
347
+ # Instantiate a new resource object
348
+ #
349
+ # @param params [Hash] Attributes, links, and relationships
350
+ def initialize(params = {})
351
+ params = params.symbolize_keys
352
+ @persisted = nil
353
+ @destroyed = nil
354
+ self.links = self.class.linker.new(params.delete(:links) || {})
355
+ self.relationships = self.class.relationship_linker.new(self.class, params.delete(:relationships) || {})
356
+ self.attributes = self.class.default_attributes.merge params.except(*self.class.prefix_params)
357
+ self.forget_change!(:type)
358
+ self.__belongs_to_params = params.slice(*self.class.prefix_params)
359
+
360
+ setup_default_properties
361
+
362
+ self.class.associations.each do |association|
363
+ if params.has_key?(association.attr_name.to_s)
364
+ set_attribute(association.attr_name, params[association.attr_name.to_s])
365
+ end
366
+ end
367
+ self.request_params = self.class.request_params_class.new(self.class)
368
+ end
369
+
370
+ # Set the current attributes and try to save them
371
+ #
372
+ # @param attrs [Hash] Attributes to update
373
+ # @return [Boolean] Whether the update succeeded or not
374
+ def update_attributes(attrs = {})
375
+ self.attributes = attrs
376
+ save
377
+ end
378
+
379
+ # Alias to update_attributes
380
+ #
381
+ # @param attrs [Hash] Attributes to update
382
+ # @return [Boolean] Whether the update succeeded or not
383
+ def update(attrs = {})
384
+ update_attributes(attrs)
385
+ end
386
+
387
+ # Mark the record as persisted
388
+ def mark_as_persisted!
389
+ @persisted = true
390
+ end
391
+
392
+ # Whether or not this record has been persisted to the database previously
393
+ #
394
+ # @return [Boolean]
395
+ def persisted?
396
+ !!@persisted && !destroyed? && has_attribute?(self.class.primary_key)
397
+ end
398
+
399
+ # Mark the record as destroyed
400
+ def mark_as_destroyed!
401
+ @destroyed = true
402
+ end
403
+
404
+ # Whether or not this record has been destroyed to the database previously
405
+ #
406
+ # @return [Boolean]
407
+ def destroyed?
408
+ !!@destroyed
409
+ end
410
+
411
+ # Returns true if this is a new record (never persisted to the database)
412
+ #
413
+ # @return [Boolean]
414
+ def new_record?
415
+ !persisted? && !destroyed?
416
+ end
417
+
418
+ # When we represent this resource as a relationship, we do so with id & type
419
+ #
420
+ # @return [Hash] Representation of this object as a relation
421
+ def as_relation
422
+ attributes.slice(:type, self.class.primary_key)
423
+ end
424
+
425
+ # When we represent this resource for serialization (create/update), we do so
426
+ # with this implementation
427
+ #
428
+ # @return [Hash] Representation of this object as JSONAPI object
429
+ def as_json_api(*)
430
+ attributes.slice(self.class.primary_key, :type).tap do |h|
431
+ relationships_for_serialization.tap do |r|
432
+ h[:relationships] = self.class.key_formatter.format_keys(r) unless r.empty?
433
+ end
434
+ h[:attributes] = self.class.key_formatter.format_keys(attributes_for_serialization)
435
+ end
436
+ end
437
+
438
+ def as_json(*)
439
+ attributes.slice(self.class.primary_key, :type).tap do |h|
440
+ relationships.as_json.tap do |r|
441
+ h[:relationships] = r unless r.empty?
442
+ end
443
+ h[:attributes] = attributes.except(self.class.primary_key, :type).as_json
444
+ end
445
+ end
446
+
447
+ # Mark all attributes for this record as dirty
448
+ def set_all_dirty!
449
+ set_all_attributes_dirty
450
+ relationships.set_all_attributes_dirty if relationships
451
+ end
452
+
453
+ def valid?(context = nil)
454
+ context ||= (new_record? ? :create : :update)
455
+ super(context)
456
+ end
457
+
458
+ # Commit the current changes to the resource to the remote server.
459
+ # If the resource was previously loaded from the server, we will
460
+ # try to update the record. Otherwise if it's a new record, then
461
+ # we will try to create it
462
+ #
463
+ # @return [Boolean] Whether or not the save succeeded
464
+ def save
465
+ return false unless valid?
466
+ raise JsonApiClient::Errors::ResourceImmutableError if _immutable
467
+
468
+ self.last_result_set = if persisted?
469
+ self.class.requestor.update(self)
470
+ else
471
+ self.class.requestor.create(self)
472
+ end
473
+
474
+ if last_result_set.has_errors?
475
+ fill_errors
476
+ false
477
+ else
478
+ self.errors.clear if self.errors
479
+ self.request_params.clear unless self.class.keep_request_params
480
+ mark_as_persisted!
481
+ if updated = last_result_set.first
482
+ self.attributes = updated.attributes
483
+ self.links.attributes = updated.links.attributes
484
+ self.relationships.attributes = updated.relationships.attributes
485
+ clear_changes_information
486
+ self.relationships.clear_changes_information
487
+ _clear_cached_relationships
488
+ end
489
+ true
490
+ end
491
+ end
492
+
493
+ # Try to destroy this resource
494
+ #
495
+ # @return [Boolean] Whether or not the destroy succeeded
496
+ def destroy
497
+ raise JsonApiClient::Errors::ResourceImmutableError if _immutable
498
+
499
+ self.last_result_set = self.class.requestor.destroy(self)
500
+ if last_result_set.has_errors?
501
+ fill_errors
502
+ false
503
+ else
504
+ mark_as_destroyed!
505
+ _clear_cached_relationships
506
+ _clear_belongs_to_params
507
+ true
508
+ end
509
+ end
510
+
511
+ def inspect
512
+ "#<#{self.class.name}:@attributes=#{attributes.inspect}>"
513
+ end
514
+
515
+ def request_includes(*includes)
516
+ self.request_params.add_includes(includes)
517
+ self
518
+ end
519
+
520
+ def reset_request_includes!
521
+ self.request_params.reset_includes!
522
+ self
523
+ end
524
+
525
+ def request_select(*fields)
526
+ fields_by_type = fields.extract_options!
527
+ fields_by_type[type.to_sym] = fields if fields.any?
528
+ fields_by_type.each do |field_type, field_names|
529
+ self.request_params.set_fields(field_type, field_names)
530
+ end
531
+ self
532
+ end
533
+
534
+ def reset_request_select!(*resource_types)
535
+ resource_types = self.request_params.field_types if resource_types.empty?
536
+ resource_types.each { |resource_type| self.request_params.remove_fields(resource_type) }
537
+ self
538
+ end
539
+
540
+ def path_attributes
541
+ _belongs_to_params.merge attributes.slice( self.class.primary_key ).symbolize_keys
542
+ end
543
+
544
+ protected
545
+
546
+ def setup_default_properties
547
+ self.class.schema.each_property do |property|
548
+ unless attributes.has_key?(property.name) || property.default.nil?
549
+ attribute_will_change!(property.name) if add_defaults_to_changes
550
+ attributes[property.name] = property.default
551
+ end
552
+ end
553
+ end
554
+
555
+ def relationship_definition_for(name)
556
+ relationships[name] if relationships && relationships.has_attribute?(name)
557
+ end
558
+
559
+ def included_data_for(name, relationship_definition)
560
+ last_result_set.included.data_for(name, relationship_definition)
561
+ end
562
+
563
+ def relationship_data_for(name, relationship_definition)
564
+ # look in included data
565
+ if relationship_definition.key?("data")
566
+ if relationships.attribute_changed?(name)
567
+ return relation_objects_for(name, relationship_definition)
568
+ else
569
+ return included_data_for(name, relationship_definition)
570
+ end
571
+ end
572
+
573
+ return unless links = relationship_definition["links"]
574
+ return unless url = links["related"]
575
+
576
+ association_for(name).data(url)
577
+ end
578
+
579
+ def relation_objects_for(name, relationship_definition)
580
+ data = relationship_definition["data"]
581
+ assoc = association_for(name)
582
+ return if data.nil? || assoc.nil?
583
+ assoc.load_records(data)
584
+ end
585
+
586
+ def method_missing(method, *args)
587
+ relationship_definition = relationship_definition_for(method)
588
+
589
+ return super unless relationship_definition
590
+
591
+ relationship_data_for(method, relationship_definition)
592
+ end
593
+
594
+ def respond_to_missing?(symbol, include_all = false)
595
+ return true if relationships && relationships.has_attribute?(symbol)
596
+ return true if association_for(symbol)
597
+ super
598
+ end
599
+
600
+ def set_attribute(name, value)
601
+ property = property_for(name)
602
+ value = property.cast(value) if property
603
+ super(name, value)
604
+ end
605
+
606
+ def has_attribute?(attr_name)
607
+ !!property_for(attr_name) || super
608
+ end
609
+
610
+ def property_for(name)
611
+ self.class.schema.find(name)
612
+ end
613
+
614
+ def association_for(name)
615
+ self.class.associations.detect do |association|
616
+ association.attr_name.to_s == self.class.key_formatter.unformat(name)
617
+ end
618
+ end
619
+
620
+ def non_serializing_attributes
621
+ self.class.read_only_attributes
622
+ end
623
+
624
+ def attributes_for_serialization
625
+ attributes.except(*non_serializing_attributes).slice(*changed)
626
+ end
627
+
628
+ def relationships_for_serialization
629
+ relationships.as_json_api
630
+ end
631
+
632
+ def error_message_for(error)
633
+ error.error_msg
634
+ end
635
+
636
+ def fill_errors
637
+ last_result_set.errors.each do |error|
638
+ key = self.class.key_formatter.unformat(error.error_key)
639
+ errors.add(key, error_message_for(error))
640
+ end
641
+ end
642
+ end
643
+ end