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