jsonapi-resources 0.6.2 → 0.7.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.
- checksums.yaml +4 -4
- data/README.md +109 -3
- data/lib/jsonapi/acts_as_resource_controller.rb +7 -7
- data/lib/jsonapi/error.rb +2 -1
- data/lib/jsonapi/exceptions.rb +8 -1
- data/lib/jsonapi/link_builder.rb +2 -2
- data/lib/jsonapi/relationship.rb +4 -3
- data/lib/jsonapi/request.rb +1 -6
- data/lib/jsonapi/resource.rb +142 -98
- data/lib/jsonapi/resource_serializer.rb +7 -7
- data/lib/jsonapi/resources/version.rb +1 -1
- data/lib/jsonapi/response_document.rb +2 -2
- data/test/controllers/controller_test.rb +77 -33
- data/test/fixtures/active_record.rb +169 -93
- data/test/fixtures/book_authors.yml +3 -0
- data/test/integration/requests/request_test.rb +63 -57
- data/test/test_helper.rb +7 -0
- data/test/unit/resource/resource_test.rb +69 -3
- data/test/unit/serializer/serializer_test.rb +1 -1
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 32e7bab3bce04e8ef68e0e58fd0274b128d7b68c
|
4
|
+
data.tar.gz: d12938d652c1ea0c692fea2c940354e673f75678
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: dd1e03599ab80a412cbe06e8d8d3231131a221f3406d82432c05d1b00c38b5002b3f1b30791c23328e613f12638f55ca35177439465ee1359dcb62f96a256057
|
7
|
+
data.tar.gz: 499fcf89189161652538b1716db928efe228b0db2e0d377012a0f98439463c1ef334cfc18b842a737f71e8c53aa9f2925932035cdbef3573726edcfb780d464b
|
data/README.md
CHANGED
@@ -340,7 +340,8 @@ end
|
|
340
340
|
|
341
341
|
##### Custom resource key validators
|
342
342
|
|
343
|
-
If you need more control over the key, you can override the #verify_key method on your resource, or set a lambda that
|
343
|
+
If you need more control over the key, you can override the #verify_key method on your resource, or set a lambda that
|
344
|
+
accepts key and context arguments in `config/initializers/jsonapi_resources.rb`:
|
344
345
|
|
345
346
|
```ruby
|
346
347
|
JSONAPI.configure do |config|
|
@@ -361,6 +362,38 @@ class AuthorResource < JSONAPI::Resource
|
|
361
362
|
end
|
362
363
|
```
|
363
364
|
|
365
|
+
#### Model Hints
|
366
|
+
|
367
|
+
Resource instances are created from model records. The determination of the correct resource type is performed using a
|
368
|
+
simple rule based on the model's name. The name is used to find a resource in the same module (as the originating
|
369
|
+
resource) that matches the name. This usually works quite well, however it can fail when model names do not match
|
370
|
+
resource names. It can also fail when using namespaced models. In this case a `model_hint` can be created to map model
|
371
|
+
names to resources. For example:
|
372
|
+
|
373
|
+
```ruby
|
374
|
+
class AuthorResource < JSONAPI::Resource
|
375
|
+
attribute :name
|
376
|
+
model_name 'Person'
|
377
|
+
model_hint model: Commenter, resource: :special_person
|
378
|
+
|
379
|
+
has_many :posts
|
380
|
+
has_many :commenters
|
381
|
+
end
|
382
|
+
```
|
383
|
+
|
384
|
+
Note that when `model_name` is set a corresponding `model_hint` is also added. This can be skipped by using the
|
385
|
+
`add_model_hint` option set to false. For example:
|
386
|
+
|
387
|
+
```ruby
|
388
|
+
class AuthorResource < JSONAPI::Resource
|
389
|
+
model_name 'Legacy::Person', add_model_hint: false
|
390
|
+
end
|
391
|
+
```
|
392
|
+
|
393
|
+
Model hints inherit from parent resources, but are not global in scope. The `model_hint` method accepts `model` and
|
394
|
+
`resource` named parameters. `model` takes an ActiveRecord class or class name (defaults to the model name), and
|
395
|
+
`resource` takes a resource type or a resource class (defaults to the current resource's type).
|
396
|
+
|
364
397
|
#### Relationships
|
365
398
|
|
366
399
|
Related resources need to be specified in the resource. These may be declared with the `relationship` or the `has_one`
|
@@ -509,11 +542,65 @@ end
|
|
509
542
|
|
510
543
|
The default value is used as if it came from the request.
|
511
544
|
|
545
|
+
##### Applying Filters
|
546
|
+
|
547
|
+
You may customize how a filter behaves by supplying a callable to the `:apply` option. This callable will be used to
|
548
|
+
apply that filter. The callable is passed the `records`, which is an `ActiveRecord::Relation`, the `value`, and an
|
549
|
+
`_options` hash. It is expected to return an `ActiveRecord::Relation`.
|
550
|
+
|
551
|
+
This example shows how you can implement different approaches for different filters.
|
552
|
+
|
553
|
+
```ruby
|
554
|
+
filter :visibility, apply: ->(records, value, _options) {
|
555
|
+
records.where('users.publicly_visible = ?', value == :public)
|
556
|
+
}
|
557
|
+
```
|
558
|
+
|
559
|
+
If you omit the `apply` callable the filter will be applied as `records.where(filter => value)`.
|
560
|
+
|
561
|
+
Note: It is also possible to override the `self.apply_filter` method, though this approach is now deprecated:
|
562
|
+
|
563
|
+
```ruby
|
564
|
+
def self.apply_filter(records, filter, value, options)
|
565
|
+
case filter
|
566
|
+
when :last_name, :first_name, :name
|
567
|
+
if value.is_a?(Array)
|
568
|
+
value.each do |val|
|
569
|
+
records = records.where(_model_class.arel_table[filter].matches(val))
|
570
|
+
end
|
571
|
+
return records
|
572
|
+
else
|
573
|
+
records.where(_model_class.arel_table[filter].matches(value))
|
574
|
+
end
|
575
|
+
else
|
576
|
+
return super(records, filter, value)
|
577
|
+
end
|
578
|
+
end
|
579
|
+
```
|
580
|
+
|
581
|
+
##### Verifying Filters
|
582
|
+
|
583
|
+
Because filters typically come straight from the request, it's prudent to verify their values. To do so, provide a
|
584
|
+
callable to the `verify` option. This callable will be passed the `value` and the `context`. Verify should return the
|
585
|
+
verified value, which may be modified.
|
586
|
+
|
587
|
+
```ruby
|
588
|
+
filter :ids,
|
589
|
+
verify: ->(values, context) {
|
590
|
+
verify_keys(values, context)
|
591
|
+
return values
|
592
|
+
},
|
593
|
+
apply: -> (records, value, _options) {
|
594
|
+
records.where('id IN (?)', value)
|
595
|
+
}
|
596
|
+
```
|
597
|
+
|
512
598
|
##### Finders
|
513
599
|
|
514
600
|
Basic finding by filters is supported by resources. This is implemented in the `find` and `find_by_key` finder methods.
|
515
601
|
Currently this is implemented for `ActiveRecord` based resources. The finder methods rely on the `records` method to get
|
516
|
-
an `
|
602
|
+
an `ActiveRecord::Relation` relation. It is therefore possible to override `records` to affect the three find related
|
603
|
+
methods.
|
517
604
|
|
518
605
|
###### Customizing base records for finder methods
|
519
606
|
|
@@ -572,7 +659,6 @@ class BaseResource < JSONAPI::Resource
|
|
572
659
|
end
|
573
660
|
```
|
574
661
|
|
575
|
-
|
576
662
|
###### Raising Errors
|
577
663
|
|
578
664
|
Inside the finder methods (like `records_for`) or inside of resource callbacks
|
@@ -635,6 +721,26 @@ def self.apply_filter(records, filter, value, options)
|
|
635
721
|
end
|
636
722
|
```
|
637
723
|
|
724
|
+
|
725
|
+
###### Applying Sorting
|
726
|
+
|
727
|
+
You can override the `apply_sort` method to gain control over how the sorting is done. This may be useful in case you'd
|
728
|
+
like to base the sorting on variables in your context.
|
729
|
+
|
730
|
+
Example:
|
731
|
+
|
732
|
+
```ruby
|
733
|
+
def self.apply_sort(records, order_options, context = {})
|
734
|
+
if order_options.has?(:trending)
|
735
|
+
records = records.order_by_trending_scope
|
736
|
+
order_options - [:trending]
|
737
|
+
end
|
738
|
+
|
739
|
+
super(records, order_options, context)
|
740
|
+
end
|
741
|
+
```
|
742
|
+
|
743
|
+
|
638
744
|
###### Override finder methods
|
639
745
|
|
640
746
|
Finally if you have more complex requirements for finding you can override the `find` and `find_by_key` methods on the
|
@@ -64,12 +64,12 @@ module JSONAPI
|
|
64
64
|
render_results(operation_results)
|
65
65
|
end
|
66
66
|
|
67
|
+
rescue => e
|
68
|
+
handle_exceptions(e)
|
69
|
+
ensure
|
67
70
|
if response.body.size > 0
|
68
71
|
response.headers['Content-Type'] = JSONAPI::MEDIA_TYPE
|
69
72
|
end
|
70
|
-
|
71
|
-
rescue => e
|
72
|
-
handle_exceptions(e)
|
73
73
|
end
|
74
74
|
|
75
75
|
# set the operations processor in the configuration or override this to use another operations processor
|
@@ -178,10 +178,10 @@ module JSONAPI
|
|
178
178
|
case e
|
179
179
|
when JSONAPI::Exceptions::Error
|
180
180
|
render_errors(e.errors)
|
181
|
-
else
|
182
|
-
|
183
|
-
|
184
|
-
|
181
|
+
else
|
182
|
+
internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
|
183
|
+
Rails.logger.error { "Internal Server Error: #{e.message} #{e.backtrace.join("\n")}" }
|
184
|
+
render_errors(internal_server_error.errors)
|
185
185
|
end
|
186
186
|
end
|
187
187
|
|
data/lib/jsonapi/error.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module JSONAPI
|
2
2
|
class Error
|
3
|
-
attr_accessor :title, :detail, :id, :href, :code, :source, :links, :status
|
3
|
+
attr_accessor :title, :detail, :id, :href, :code, :source, :links, :status, :meta
|
4
4
|
|
5
5
|
def initialize(options = {})
|
6
6
|
@title = options[:title]
|
@@ -16,6 +16,7 @@ module JSONAPI
|
|
16
16
|
@links = options[:links]
|
17
17
|
|
18
18
|
@status = Rack::Utils::SYMBOL_TO_STATUS_CODE[options[:status]].to_s
|
19
|
+
@meta = options[:meta]
|
19
20
|
end
|
20
21
|
end
|
21
22
|
|
data/lib/jsonapi/exceptions.rb
CHANGED
@@ -10,10 +10,17 @@ module JSONAPI
|
|
10
10
|
end
|
11
11
|
|
12
12
|
def errors
|
13
|
+
unless Rails.env.production?
|
14
|
+
meta = Hash.new
|
15
|
+
meta[:exception] = exception.message
|
16
|
+
meta[:backtrace] = exception.backtrace
|
17
|
+
end
|
18
|
+
|
13
19
|
[JSONAPI::Error.new(code: JSONAPI::INTERNAL_SERVER_ERROR,
|
14
20
|
status: :internal_server_error,
|
15
21
|
title: 'Internal Server Error',
|
16
|
-
detail: 'Internal Server Error'
|
22
|
+
detail: 'Internal Server Error',
|
23
|
+
meta: meta)]
|
17
24
|
end
|
18
25
|
end
|
19
26
|
|
data/lib/jsonapi/link_builder.rb
CHANGED
@@ -116,7 +116,7 @@ module JSONAPI
|
|
116
116
|
def regular_primary_resources_path
|
117
117
|
[
|
118
118
|
formatted_module_path_from_class(primary_resource_klass),
|
119
|
-
|
119
|
+
format_route(primary_resource_klass._type.to_s),
|
120
120
|
].join
|
121
121
|
end
|
122
122
|
|
@@ -127,7 +127,7 @@ module JSONAPI
|
|
127
127
|
def regular_resource_path(source)
|
128
128
|
[
|
129
129
|
formatted_module_path_from_class(source.class),
|
130
|
-
|
130
|
+
format_route(source.class._type.to_s),
|
131
131
|
"/#{ source.id }",
|
132
132
|
].join
|
133
133
|
end
|
data/lib/jsonapi/relationship.rb
CHANGED
@@ -1,14 +1,15 @@
|
|
1
1
|
module JSONAPI
|
2
2
|
class Relationship
|
3
3
|
attr_reader :acts_as_set, :foreign_key, :type, :options, :name,
|
4
|
-
:class_name, :polymorphic, :always_include_linkage_data
|
4
|
+
:class_name, :polymorphic, :always_include_linkage_data,
|
5
|
+
:parent_resource
|
5
6
|
|
6
7
|
def initialize(name, options = {})
|
7
8
|
@name = name.to_s
|
8
9
|
@options = options
|
9
10
|
@acts_as_set = options.fetch(:acts_as_set, false) == true
|
10
11
|
@foreign_key = options[:foreign_key] ? options[:foreign_key].to_sym : nil
|
11
|
-
@
|
12
|
+
@parent_resource = options[:parent_resource]
|
12
13
|
@relation_name = options.fetch(:relation_name, @name)
|
13
14
|
@polymorphic = options.fetch(:polymorphic, false) == true
|
14
15
|
@always_include_linkage_data = options.fetch(:always_include_linkage_data, false) == true
|
@@ -21,7 +22,7 @@ module JSONAPI
|
|
21
22
|
end
|
22
23
|
|
23
24
|
def resource_klass
|
24
|
-
@resource_klass
|
25
|
+
@resource_klass = @parent_resource.resource_for(@class_name)
|
25
26
|
end
|
26
27
|
|
27
28
|
def relation_name(options)
|
data/lib/jsonapi/request.rb
CHANGED
@@ -463,7 +463,7 @@ module JSONAPI
|
|
463
463
|
|
464
464
|
unless links_object[:id].nil?
|
465
465
|
resource = self.resource_klass || Resource
|
466
|
-
relationship_resource = resource.resource_for(
|
466
|
+
relationship_resource = resource.resource_for(unformat_key(links_object[:type]).to_s)
|
467
467
|
relationship_id = relationship_resource.verify_key(links_object[:id], @context)
|
468
468
|
if relationship.polymorphic?
|
469
469
|
{ id: relationship_id, type: unformat_key(links_object[:type].to_s) }
|
@@ -610,11 +610,6 @@ module JSONAPI
|
|
610
610
|
def parse_single_replace_operation(data, keys, id_key_presence_check_required: true)
|
611
611
|
fail JSONAPI::Exceptions::MissingKey.new if data[:id].nil?
|
612
612
|
|
613
|
-
type = data[:type]
|
614
|
-
if type.nil? || type != format_key(@resource_klass._type).to_s
|
615
|
-
fail JSONAPI::Exceptions::ParameterMissing.new(:type)
|
616
|
-
end
|
617
|
-
|
618
613
|
key = data[:id]
|
619
614
|
if id_key_presence_check_required && !keys.include?(key)
|
620
615
|
fail JSONAPI::Exceptions::KeyNotIncludedInURL.new(key)
|
data/lib/jsonapi/resource.rb
CHANGED
@@ -4,8 +4,6 @@ module JSONAPI
|
|
4
4
|
class Resource
|
5
5
|
include Callbacks
|
6
6
|
|
7
|
-
@@resource_types = {}
|
8
|
-
|
9
7
|
attr_reader :context
|
10
8
|
|
11
9
|
define_jsonapi_resources_callbacks :create,
|
@@ -277,55 +275,61 @@ module JSONAPI
|
|
277
275
|
end
|
278
276
|
|
279
277
|
class << self
|
280
|
-
def inherited(
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
278
|
+
def inherited(subclass)
|
279
|
+
subclass.abstract(false)
|
280
|
+
subclass.immutable(false)
|
281
|
+
subclass._attributes = (_attributes || {}).dup
|
282
|
+
subclass._model_hints = (_model_hints || {}).dup
|
283
|
+
|
284
|
+
subclass._relationships = {}
|
285
|
+
# Add the relationships from the base class to the subclass using the original options
|
286
|
+
if _relationships.is_a?(Hash)
|
287
|
+
_relationships.each_value do |relationship|
|
288
|
+
options = relationship.options.dup
|
289
|
+
options[:parent_resource] = subclass
|
290
|
+
subclass._add_relationship(relationship.class, relationship.name, options)
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
subclass._allowed_filters = (_allowed_filters || Set.new).dup
|
295
|
+
|
296
|
+
type = subclass.name.demodulize.sub(/Resource$/, '').underscore
|
297
|
+
subclass._type = type.pluralize.to_sym
|
298
|
+
|
299
|
+
subclass.attribute :id, format: :id
|
300
|
+
|
301
|
+
check_reserved_resource_name(subclass._type, subclass.name)
|
302
|
+
end
|
303
|
+
|
304
|
+
def resource_for(type)
|
305
|
+
type_with_module = type.include?('/') ? type : module_path + type
|
306
|
+
|
307
|
+
resource_name = _resource_name_from_type(type_with_module)
|
308
|
+
resource = resource_name.safe_constantize if resource_name
|
309
|
+
if resource.nil?
|
310
|
+
fail NameError, "JSONAPI: Could not find resource '#{type}'. (Class #{resource_name} not found)"
|
308
311
|
end
|
309
|
-
|
312
|
+
resource
|
310
313
|
end
|
311
314
|
|
312
|
-
def
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
}
|
318
|
-
|
319
|
-
|
315
|
+
def resource_for_model(model)
|
316
|
+
resource_for(resource_type_for(model))
|
317
|
+
end
|
318
|
+
|
319
|
+
def _resource_name_from_type(type)
|
320
|
+
"#{type.to_s.underscore.singularize}_resource".camelize
|
321
|
+
end
|
322
|
+
|
323
|
+
def resource_type_for(model)
|
324
|
+
model_name = model.class.to_s.underscore
|
325
|
+
if _model_hints[model_name]
|
326
|
+
_model_hints[model_name]
|
320
327
|
else
|
321
|
-
|
322
|
-
fail NameError,
|
323
|
-
"JSONAPI: Could not find resource for model '#{path}#{normalized_model}'"
|
324
|
-
#:nocov:#
|
328
|
+
model_name.rpartition('/').last
|
325
329
|
end
|
326
330
|
end
|
327
331
|
|
328
|
-
attr_accessor :_attributes, :_relationships, :_allowed_filters, :_type, :_paginator
|
332
|
+
attr_accessor :_attributes, :_relationships, :_allowed_filters, :_type, :_paginator, :_model_hints
|
329
333
|
|
330
334
|
def create(context)
|
331
335
|
new(create_model, context)
|
@@ -396,8 +400,17 @@ module JSONAPI
|
|
396
400
|
_add_relationship(Relationship::ToMany, *attrs)
|
397
401
|
end
|
398
402
|
|
399
|
-
def model_name(model)
|
403
|
+
def model_name(model, options = {})
|
400
404
|
@_model_name = model.to_sym
|
405
|
+
|
406
|
+
model_hint(model: @_model_name, resource: self) unless options[:add_model_hint] == false
|
407
|
+
end
|
408
|
+
|
409
|
+
def model_hint(model: _model_name, resource: _type)
|
410
|
+
model_name = ((model.is_a?(Class)) && (model < ActiveRecord::Base)) ? model.name : model
|
411
|
+
resource_type = ((resource.is_a?(Class)) && (resource < JSONAPI::Resource)) ? resource._type : resource.to_s
|
412
|
+
|
413
|
+
_model_hints[model_name.to_s.gsub('::', '/').underscore] = resource_type.to_s
|
401
414
|
end
|
402
415
|
|
403
416
|
def filters(*attrs)
|
@@ -486,7 +499,7 @@ module JSONAPI
|
|
486
499
|
records
|
487
500
|
end
|
488
501
|
|
489
|
-
def apply_sort(records, order_options)
|
502
|
+
def apply_sort(records, order_options, _context = {})
|
490
503
|
if order_options.any?
|
491
504
|
records.order(order_options)
|
492
505
|
else
|
@@ -494,8 +507,14 @@ module JSONAPI
|
|
494
507
|
end
|
495
508
|
end
|
496
509
|
|
497
|
-
def apply_filter(records, filter, value,
|
498
|
-
|
510
|
+
def apply_filter(records, filter, value, options = {})
|
511
|
+
strategy = _allowed_filters.fetch(filter.to_sym, Hash.new)[:apply]
|
512
|
+
|
513
|
+
if strategy
|
514
|
+
strategy.call(records, value, options)
|
515
|
+
else
|
516
|
+
records.where(filter => value)
|
517
|
+
end
|
499
518
|
end
|
500
519
|
|
501
520
|
def apply_filters(records, filters, options = {})
|
@@ -528,8 +547,8 @@ module JSONAPI
|
|
528
547
|
apply_includes(records, options)
|
529
548
|
end
|
530
549
|
|
531
|
-
def sort_records(records, order_options)
|
532
|
-
apply_sort(records, order_options)
|
550
|
+
def sort_records(records, order_options, context = {})
|
551
|
+
apply_sort(records, order_options, context)
|
533
552
|
end
|
534
553
|
|
535
554
|
def find_count(filters, options = {})
|
@@ -544,13 +563,13 @@ module JSONAPI
|
|
544
563
|
|
545
564
|
sort_criteria = options.fetch(:sort_criteria) { [] }
|
546
565
|
order_options = construct_order_options(sort_criteria)
|
547
|
-
records = sort_records(records, order_options)
|
566
|
+
records = sort_records(records, order_options, context)
|
548
567
|
|
549
568
|
records = apply_pagination(records, options[:paginator], order_options)
|
550
569
|
|
551
570
|
resources = []
|
552
571
|
records.each do |model|
|
553
|
-
resources.push
|
572
|
+
resources.push self.resource_for_model(model).new(model, context)
|
554
573
|
end
|
555
574
|
|
556
575
|
resources
|
@@ -562,7 +581,7 @@ module JSONAPI
|
|
562
581
|
records = apply_includes(records, options)
|
563
582
|
model = records.where({_primary_key => key}).first
|
564
583
|
fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil?
|
565
|
-
|
584
|
+
self.resource_for_model(model).new(model, context)
|
566
585
|
end
|
567
586
|
|
568
587
|
# Override this method if you want to customize the relation for
|
@@ -588,10 +607,16 @@ module JSONAPI
|
|
588
607
|
filter_values = []
|
589
608
|
filter_values += CSV.parse_line(raw) unless raw.nil? || raw.empty?
|
590
609
|
|
591
|
-
|
592
|
-
|
610
|
+
strategy = _allowed_filters.fetch(filter, Hash.new)[:verify]
|
611
|
+
|
612
|
+
if strategy
|
613
|
+
[filter, strategy.call(filter_values, context)]
|
593
614
|
else
|
594
|
-
|
615
|
+
if is_filter_relationship?(filter)
|
616
|
+
verify_relationship_filter(filter, filter_values, context)
|
617
|
+
else
|
618
|
+
verify_custom_filter(filter, filter_values, context)
|
619
|
+
end
|
595
620
|
end
|
596
621
|
end
|
597
622
|
|
@@ -638,12 +663,13 @@ module JSONAPI
|
|
638
663
|
end
|
639
664
|
end
|
640
665
|
|
641
|
-
# override to allow for custom filters
|
666
|
+
# Either add a custom :verify labmda or override verify_custom_filter to allow for custom filters
|
642
667
|
def verify_custom_filter(filter, value, _context = nil)
|
643
668
|
[filter, value]
|
644
669
|
end
|
645
670
|
|
646
|
-
#
|
671
|
+
# Either add a custom :verify labmda or override verify_relationship_filter to allow for custom
|
672
|
+
# relationship logic, such as uuids, multiple keys or permission checks on keys
|
647
673
|
def verify_relationship_filter(filter, raw, _context = nil)
|
648
674
|
[filter, raw]
|
649
675
|
end
|
@@ -663,7 +689,7 @@ module JSONAPI
|
|
663
689
|
end
|
664
690
|
|
665
691
|
def _model_name
|
666
|
-
@_model_name ||= name.demodulize.sub(/Resource$/, '')
|
692
|
+
_abstract ? '' : @_model_name ||= name.demodulize.sub(/Resource$/, '')
|
667
693
|
end
|
668
694
|
|
669
695
|
def _primary_key
|
@@ -720,7 +746,11 @@ module JSONAPI
|
|
720
746
|
end
|
721
747
|
|
722
748
|
def module_path
|
723
|
-
name
|
749
|
+
if name == 'JSONAPI::Resource'
|
750
|
+
''
|
751
|
+
else
|
752
|
+
name =~ /::[^:]+\Z/ ? ($`.freeze.gsub('::', '/') + '/').underscore : ''
|
753
|
+
end
|
724
754
|
end
|
725
755
|
|
726
756
|
def construct_order_options(sort_params)
|
@@ -732,49 +762,28 @@ module JSONAPI
|
|
732
762
|
end
|
733
763
|
end
|
734
764
|
|
735
|
-
private
|
736
|
-
|
737
|
-
def check_reserved_resource_name(type, name)
|
738
|
-
if [:ids, :types, :hrefs, :links].include?(type)
|
739
|
-
warn "[NAME COLLISION] `#{name}` is a reserved resource name."
|
740
|
-
return
|
741
|
-
end
|
742
|
-
end
|
743
|
-
|
744
|
-
def check_reserved_attribute_name(name)
|
745
|
-
# Allow :id since it can be used to specify the format. Since it is a method on the base Resource
|
746
|
-
# an attribute method won't be created for it.
|
747
|
-
if [:type].include?(name.to_sym)
|
748
|
-
warn "[NAME COLLISION] `#{name}` is a reserved key in #{@@resource_types[_type]}."
|
749
|
-
end
|
750
|
-
end
|
751
|
-
|
752
|
-
def check_reserved_relationship_name(name)
|
753
|
-
if [:id, :ids, :type, :types].include?(name.to_sym)
|
754
|
-
warn "[NAME COLLISION] `#{name}` is a reserved relationship name in #{@@resource_types[_type]}."
|
755
|
-
end
|
756
|
-
end
|
757
|
-
|
758
765
|
def _add_relationship(klass, *attrs)
|
759
766
|
options = attrs.extract_options!
|
760
|
-
options[:
|
767
|
+
options[:parent_resource] = self
|
761
768
|
|
762
769
|
attrs.each do |attr|
|
763
|
-
|
770
|
+
relationship_name = attr.to_sym
|
771
|
+
|
772
|
+
check_reserved_relationship_name(relationship_name)
|
764
773
|
|
765
774
|
# Initialize from an ActiveRecord model's properties
|
766
775
|
if _model_class && _model_class.ancestors.collect{|ancestor| ancestor.name}.include?('ActiveRecord::Base')
|
767
|
-
model_association = _model_class.reflect_on_association(
|
776
|
+
model_association = _model_class.reflect_on_association(relationship_name)
|
768
777
|
if model_association
|
769
778
|
options[:class_name] ||= model_association.class_name
|
770
779
|
end
|
771
780
|
end
|
772
781
|
|
773
|
-
@_relationships[
|
782
|
+
@_relationships[relationship_name] = relationship = klass.new(relationship_name, options)
|
774
783
|
|
775
784
|
associated_records_method_name = case relationship
|
776
|
-
when JSONAPI::Relationship::ToOne then "record_for_#{
|
777
|
-
when JSONAPI::Relationship::ToMany then "records_for_#{
|
785
|
+
when JSONAPI::Relationship::ToOne then "record_for_#{relationship_name}"
|
786
|
+
when JSONAPI::Relationship::ToMany then "records_for_#{relationship_name}"
|
778
787
|
end
|
779
788
|
|
780
789
|
foreign_key = relationship.foreign_key
|
@@ -784,6 +793,7 @@ module JSONAPI
|
|
784
793
|
end unless method_defined?("#{foreign_key}=")
|
785
794
|
|
786
795
|
define_method associated_records_method_name do
|
796
|
+
relationship = self.class._relationships[relationship_name]
|
787
797
|
relation_name = relationship.relation_name(context: @context)
|
788
798
|
records_for(relation_name)
|
789
799
|
end unless method_defined?(associated_records_method_name)
|
@@ -794,10 +804,12 @@ module JSONAPI
|
|
794
804
|
@model.method(foreign_key).call
|
795
805
|
end unless method_defined?(foreign_key)
|
796
806
|
|
797
|
-
define_method
|
807
|
+
define_method relationship_name do |options = {}|
|
808
|
+
relationship = self.class._relationships[relationship_name]
|
809
|
+
|
798
810
|
if relationship.polymorphic?
|
799
811
|
associated_model = public_send(associated_records_method_name)
|
800
|
-
resource_klass = self.class.
|
812
|
+
resource_klass = self.class.resource_for_model(associated_model) if associated_model
|
801
813
|
return resource_klass.new(associated_model, @context) if resource_klass
|
802
814
|
else
|
803
815
|
resource_klass = relationship.resource_klass
|
@@ -806,21 +818,25 @@ module JSONAPI
|
|
806
818
|
return associated_model ? resource_klass.new(associated_model, @context) : nil
|
807
819
|
end
|
808
820
|
end
|
809
|
-
end unless method_defined?(
|
821
|
+
end unless method_defined?(relationship_name)
|
810
822
|
else
|
811
823
|
define_method foreign_key do
|
824
|
+
relationship = self.class._relationships[relationship_name]
|
825
|
+
|
812
826
|
record = public_send(associated_records_method_name)
|
813
827
|
return nil if record.nil?
|
814
828
|
record.public_send(relationship.resource_klass._primary_key)
|
815
829
|
end unless method_defined?(foreign_key)
|
816
830
|
|
817
|
-
define_method
|
831
|
+
define_method relationship_name do |options = {}|
|
832
|
+
relationship = self.class._relationships[relationship_name]
|
833
|
+
|
818
834
|
resource_klass = relationship.resource_klass
|
819
835
|
if resource_klass
|
820
836
|
associated_model = public_send(associated_records_method_name)
|
821
837
|
return associated_model ? resource_klass.new(associated_model, @context) : nil
|
822
838
|
end
|
823
|
-
end unless method_defined?(
|
839
|
+
end unless method_defined?(relationship_name)
|
824
840
|
end
|
825
841
|
elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
|
826
842
|
define_method foreign_key do
|
@@ -829,7 +845,10 @@ module JSONAPI
|
|
829
845
|
record.public_send(relationship.resource_klass._primary_key)
|
830
846
|
end
|
831
847
|
end unless method_defined?(foreign_key)
|
832
|
-
|
848
|
+
|
849
|
+
define_method relationship_name do |options = {}|
|
850
|
+
relationship = self.class._relationships[relationship_name]
|
851
|
+
|
833
852
|
resource_klass = relationship.resource_klass
|
834
853
|
records = public_send(associated_records_method_name)
|
835
854
|
|
@@ -841,7 +860,7 @@ module JSONAPI
|
|
841
860
|
sort_criteria = options.fetch(:sort_criteria, {})
|
842
861
|
unless sort_criteria.nil? || sort_criteria.empty?
|
843
862
|
order_options = relationship.resource_klass.construct_order_options(sort_criteria)
|
844
|
-
records = resource_klass.apply_sort(records, order_options)
|
863
|
+
records = resource_klass.apply_sort(records, order_options, @context)
|
845
864
|
end
|
846
865
|
|
847
866
|
paginator = options[:paginator]
|
@@ -850,13 +869,38 @@ module JSONAPI
|
|
850
869
|
end
|
851
870
|
|
852
871
|
return records.collect do |record|
|
853
|
-
|
872
|
+
if relationship.polymorphic?
|
873
|
+
resource_klass = self.class.resource_for_model(record)
|
874
|
+
end
|
854
875
|
resource_klass.new(record, @context)
|
855
876
|
end
|
856
|
-
end unless method_defined?(
|
877
|
+
end unless method_defined?(relationship_name)
|
857
878
|
end
|
858
879
|
end
|
859
880
|
end
|
881
|
+
|
882
|
+
private
|
883
|
+
|
884
|
+
def check_reserved_resource_name(type, name)
|
885
|
+
if [:ids, :types, :hrefs, :links].include?(type)
|
886
|
+
warn "[NAME COLLISION] `#{name}` is a reserved resource name."
|
887
|
+
return
|
888
|
+
end
|
889
|
+
end
|
890
|
+
|
891
|
+
def check_reserved_attribute_name(name)
|
892
|
+
# Allow :id since it can be used to specify the format. Since it is a method on the base Resource
|
893
|
+
# an attribute method won't be created for it.
|
894
|
+
if [:type].include?(name.to_sym)
|
895
|
+
warn "[NAME COLLISION] `#{name}` is a reserved key in #{_resource_name_from_type(_type)}."
|
896
|
+
end
|
897
|
+
end
|
898
|
+
|
899
|
+
def check_reserved_relationship_name(name)
|
900
|
+
if [:id, :ids, :type, :types].include?(name.to_sym)
|
901
|
+
warn "[NAME COLLISION] `#{name}` is a reserved relationship name in #{_resource_name_from_type(_type)}."
|
902
|
+
end
|
903
|
+
end
|
860
904
|
end
|
861
905
|
end
|
862
906
|
end
|