Skip to content

Commit 7e729bd

Browse files
committed
Merge pull request #284 from cerebris/contextual_relations
relation_name: choose the relation based on the context
2 parents c58a43b + 84070fc commit 7e729bd

13 files changed

Lines changed: 487 additions & 83 deletions

File tree

README.md

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,9 @@ The association methods support the following options:
243243
* `foreign_key` - the method on the resource used to fetch the related resource. Defaults to `<resource_name>_id` for
244244
has_one and `<resource_name>_ids` for has_many relationships.
245245
* `acts_as_set` - allows the entire set of related records to be replaced in one operation. Defaults to false if not set.
246-
246+
* `relation_name` - the name of the relation to use on the model. A lambda may be provided which allows conditional
247+
selection of the relation based on the context.
248+
247249
Examples:
248250

249251
```ruby
@@ -264,6 +266,25 @@ class ExpenseEntryResource < JSONAPI::Resource
264266
end
265267
```
266268

269+
```ruby
270+
class BookResource < JSONAPI::Resource
271+
272+
# Only book_admins may see unapproved comments for a book. Using
273+
# a lambda to select the correct relation on the model
274+
has_many :book_comments, relation_name: -> (options = {}) {
275+
context = options[:context]
276+
current_user = context ? context[:current_user] : nil
277+
278+
unless current_user && current_user.book_admin
279+
:approved_book_comments
280+
else
281+
:book_comments
282+
end
283+
}
284+
...
285+
end
286+
```
287+
267288
#### Filters
268289

269290
Filters for locating objects of the resource type are specified in the resource definition. Single filters can be

lib/jsonapi/acts_as_resource_controller.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ module ActsAsResourceController
66

77
included do
88
before_filter :ensure_correct_media_type, only: [:create, :update, :create_association, :update_association]
9-
before_filter :setup_request
9+
append_before_filter :setup_request
1010
after_filter :setup_response
1111
end
1212

lib/jsonapi/association.rb

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,17 +7,35 @@ def initialize(name, options={})
77
@options = options
88
@acts_as_set = options.fetch(:acts_as_set, false) == true
99
@foreign_key = options[:foreign_key ] ? options[:foreign_key ].to_sym : nil
10-
@module_path = options[:module_path] || ''
10+
@module_path = options.fetch(:module_path, '')
11+
@relation_name = options.fetch(:relation_name, @name)
1112
end
1213

1314
def primary_key
14-
@primary_key ||= Resource.resource_for(@module_path + @name)._primary_key
15+
@primary_key ||= resource_klass._primary_key
16+
end
17+
18+
def resource_klass
19+
@resource_klass ||= Resource.resource_for(@module_path + @class_name)
20+
end
21+
22+
def relation_name(options = {})
23+
case @relation_name
24+
when Symbol
25+
# :nocov:
26+
@relation_name
27+
# :nocov:
28+
when String
29+
@relation_name.to_sym
30+
when Proc
31+
@relation_name.call(options)
32+
end
1533
end
1634

1735
class HasOne < Association
1836
def initialize(name, options={})
1937
super
20-
@class_name = options.fetch(:class_name, name.to_s.capitalize)
38+
@class_name = options.fetch(:class_name, name.to_s.camelize)
2139
@type = class_name.underscore.pluralize.to_sym
2240
@foreign_key ||= "#{name}_id".to_sym
2341
end
@@ -26,7 +44,7 @@ def initialize(name, options={})
2644
class HasMany < Association
2745
def initialize(name, options={})
2846
super
29-
@class_name = options.fetch(:class_name, name.to_s.capitalize.singularize)
47+
@class_name = options.fetch(:class_name, name.to_s.camelize.singularize)
3048
@type = class_name.underscore.pluralize.to_sym
3149
@foreign_key ||= "#{name.to_s.singularize}_ids".to_sym
3250
end

lib/jsonapi/resource.rb

Lines changed: 77 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ def _create_has_many_links(association_type, association_key_values)
159159
association = self.class._associations[association_type]
160160

161161
association_key_values.each do |association_key_value|
162-
related_resource = Resource.resource_for(self.class.module_path + association.type.to_s).find_by_key(association_key_value, context: @context)
162+
related_resource = association.resource_klass.find_by_key(association_key_value, context: @context)
163163

164164
# ToDo: Add option to skip relations that already exist instead of returning an error?
165165
relation = @model.send(association.type).where(association.primary_key => association_key_value).first
@@ -364,8 +364,33 @@ def fields
364364
_associations.keys | _attributes.keys
365365
end
366366

367-
def apply_includes(records, directives)
368-
records = records.includes(*directives.model_includes) if directives
367+
def resolve_association_names_to_relations(resource_klass, model_includes, options = {})
368+
case model_includes
369+
when Array
370+
return model_includes.map do |value|
371+
resolve_association_names_to_relations(resource_klass, value, options)
372+
end
373+
when Hash
374+
model_includes.keys.each do |key|
375+
association = resource_klass._associations[key]
376+
value = model_includes[key]
377+
model_includes.delete(key)
378+
model_includes[association.relation_name(options)] = resolve_association_names_to_relations(association.resource_klass, value, options)
379+
end
380+
return model_includes
381+
when Symbol
382+
association = resource_klass._associations[model_includes]
383+
return association.relation_name(options)
384+
end
385+
end
386+
387+
def apply_includes(records, options = {})
388+
include_directives = options[:include_directives]
389+
if include_directives
390+
model_includes = resolve_association_names_to_relations(self, include_directives.model_includes, options)
391+
records = records.includes(model_includes)
392+
end
393+
369394
records
370395
end
371396

@@ -395,7 +420,7 @@ def apply_filters(records, filters, options = {})
395420
filters.each do |filter, value|
396421
if _associations.include?(filter)
397422
if _associations[filter].is_a?(JSONAPI::Association::HasMany)
398-
required_includes.push(filter)
423+
required_includes.push(filter.to_s)
399424
records = apply_filter(records, "#{filter}.#{_associations[filter].primary_key}", value, options)
400425
else
401426
records = apply_filter(records, "#{_associations[filter].foreign_key}", value, options)
@@ -407,20 +432,16 @@ def apply_filters(records, filters, options = {})
407432
end
408433

409434
if required_includes.any?
410-
records.includes(required_includes)
411-
elsif records.respond_to? :to_ary
412-
records
413-
else
414-
records.all
435+
records = apply_includes(records, options.merge(include_directives: IncludeDirectives.new(required_includes)))
415436
end
437+
438+
records
416439
end
417440

418441
def filter_records(filters, options)
419-
include_directives = options[:include_directives]
420-
421442
records = records(options)
422-
records = apply_includes(records, include_directives)
423-
apply_filters(records, filters, options)
443+
records = apply_filters(records, filters, options)
444+
apply_includes(records, options)
424445
end
425446

426447
def sort_records(records, order_options)
@@ -453,9 +474,8 @@ def find(filters, options = {})
453474

454475
def find_by_key(key, options = {})
455476
context = options[:context]
456-
include_directives = options[:include_directives]
457477
records = records(options)
458-
records = apply_includes(records, include_directives)
478+
records = apply_includes(records, options)
459479
model = records.where({_primary_key => key}).first
460480
if model.nil?
461481
raise JSONAPI::Exceptions::RecordNotFound.new(key)
@@ -555,7 +575,7 @@ def _allowed_filters
555575
def _resource_name_from_type(type)
556576
class_name = @@resource_types[type]
557577
if class_name.nil?
558-
class_name = "#{type.to_s.singularize}_resource".camelize
578+
class_name = "#{type.to_s.underscore.singularize}_resource".camelize
559579
@@resource_types[type] = class_name
560580
end
561581
return class_name
@@ -619,62 +639,70 @@ def _associate(klass, *attrs)
619639
attrs.each do |attr|
620640
check_reserved_association_name(attr)
621641

622-
@_associations[attr] = klass.new(attr, options)
642+
association = @_associations[attr] = klass.new(attr, options)
623643

624-
foreign_key = @_associations[attr].foreign_key
644+
associated_records_method_name = case association
645+
when JSONAPI::Association::HasOne then "record_for_#{attr}"
646+
when JSONAPI::Association::HasMany then "records_for_#{attr}"
647+
end
625648

626-
define_method foreign_key do
627-
@model.method(foreign_key).call
628-
end unless method_defined?(foreign_key)
649+
foreign_key = association.foreign_key
629650

630651
define_method "#{foreign_key}=" do |value|
631652
@model.method("#{foreign_key}=").call(value)
632653
end unless method_defined?("#{foreign_key}=")
633654

634-
associated_records_method_name = case @_associations[attr]
635-
when JSONAPI::Association::HasOne then "record_for_#{attr}"
636-
when JSONAPI::Association::HasMany then "records_for_#{attr}"
637-
end
638-
639655
define_method associated_records_method_name do |options={}|
640-
records_for(attr, options)
656+
relation_name = association.relation_name(options.merge({context: @context}))
657+
records_for(relation_name, options)
641658
end unless method_defined?(associated_records_method_name)
642659

643-
if @_associations[attr].is_a?(JSONAPI::Association::HasOne)
644-
define_method attr do
645-
type_name = self.class._associations[attr].type.to_s
646-
resource_class = Resource.resource_for(self.class.module_path + type_name)
647-
if resource_class
660+
if association.is_a?(JSONAPI::Association::HasOne)
661+
define_method foreign_key do
662+
@model.method(foreign_key).call
663+
end unless method_defined?(foreign_key)
664+
665+
define_method attr do |options = {}|
666+
resource_klass = association.resource_klass
667+
if resource_klass
648668
associated_model = public_send(associated_records_method_name)
649-
return associated_model ? resource_class.new(associated_model, @context) : nil
669+
return associated_model ? resource_klass.new(associated_model, @context) : nil
650670
end
651671
end unless method_defined?(attr)
652-
elsif @_associations[attr].is_a?(JSONAPI::Association::HasMany)
672+
elsif association.is_a?(JSONAPI::Association::HasMany)
673+
define_method foreign_key do
674+
records = public_send(associated_records_method_name)
675+
return records.collect do |record|
676+
record.send(association.resource_klass._primary_key)
677+
end
678+
end unless method_defined?(foreign_key)
653679
define_method attr do |options = {}|
654-
type_name = self.class._associations[attr].type.to_s
655-
resource_class = Resource.resource_for(self.class.module_path + type_name)
680+
resource_klass = association.resource_klass
681+
records = public_send(associated_records_method_name)
682+
656683
filters = options.fetch(:filters, {})
684+
unless filters.nil? || filters.empty?
685+
records = resource_klass.apply_filters(records, filters, options)
686+
end
687+
657688
sort_criteria = options.fetch(:sort_criteria, {})
658-
paginator = options[:paginator]
689+
unless sort_criteria.nil? || sort_criteria.empty?
690+
order_options = self.class.construct_order_options(sort_criteria)
691+
records = resource_klass.apply_sort(records, order_options)
692+
end
659693

660-
resources = []
694+
paginator = options[:paginator]
695+
if paginator
696+
records = resource_klass.apply_pagination(records, paginator, order_options)
697+
end
661698

662-
if resource_class
663-
records = public_send(associated_records_method_name)
664-
records = resource_class.apply_filters(records, filters, options)
665-
order_options = self.class.construct_order_options(sort_criteria)
666-
records = resource_class.apply_sort(records, order_options)
667-
records = resource_class.apply_pagination(records, paginator, order_options)
668-
records.each do |record|
669-
resources.push resource_class.new(record, @context)
670-
end
699+
return records.collect do |record|
700+
resource_klass.new(record, @context)
671701
end
672-
return resources
673702
end unless method_defined?(attr)
674703
end
675704
end
676705
end
677706
end
678-
679707
end
680708
end

0 commit comments

Comments
 (0)