Skip to content

Commit f31589a

Browse files
authored
Merge pull request #770 from cerebris/pr/741
Pr/741
2 parents f672d51 + a32e56f commit f31589a

2 files changed

Lines changed: 170 additions & 109 deletions

File tree

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
module JSONAPI
2+
class RelationshipBuilder
3+
attr_reader :model_class, :options, :relationship_class
4+
delegate :register_relationship, to: :@resource_class
5+
6+
def initialize(relationship_class, model_class, options)
7+
@relationship_class = relationship_class
8+
@model_class = model_class
9+
@resource_class = options[:parent_resource]
10+
@options = options
11+
end
12+
13+
def define_relationship_methods(relationship_name)
14+
# Initialize from an ActiveRecord model's properties
15+
if model_class && model_class.ancestors.collect{|ancestor| ancestor.name}.include?('ActiveRecord::Base')
16+
model_association = model_class.reflect_on_association(relationship_name)
17+
if model_association
18+
options[:class_name] ||= model_association.class_name
19+
end
20+
end
21+
22+
relationship = register_relationship(
23+
relationship_name,
24+
relationship_class.new(relationship_name, options)
25+
)
26+
27+
foreign_key = define_foreign_key_setter(relationship.foreign_key)
28+
29+
case relationship
30+
when JSONAPI::Relationship::ToOne
31+
associated = define_resource_relationship_accessor(:one, relationship_name)
32+
args = [relationship, foreign_key, associated, relationship_name]
33+
34+
relationship.belongs_to? ? build_belongs_to(*args) : build_has_one(*args)
35+
when JSONAPI::Relationship::ToMany
36+
associated = define_resource_relationship_accessor(:many, relationship_name)
37+
38+
build_to_many(relationship, foreign_key, associated, relationship_name)
39+
end
40+
end
41+
42+
def define_foreign_key_setter(foreign_key)
43+
define_on_resource "#{foreign_key}=" do |value|
44+
@model.method("#{foreign_key}=").call(value)
45+
end
46+
foreign_key
47+
end
48+
49+
def define_resource_relationship_accessor(type, relationship_name)
50+
associated_records_method_name = {
51+
one: "record_for_#{relationship_name}",
52+
many: "records_for_#{relationship_name}"
53+
}
54+
.fetch(type)
55+
56+
define_on_resource associated_records_method_name do
57+
relationship = self.class._relationships[relationship_name]
58+
relation_name = relationship.relation_name(context: @context)
59+
records_for(relation_name)
60+
end
61+
62+
associated_records_method_name
63+
end
64+
65+
def build_belongs_to(relationship, foreign_key, associated_records_method_name, relationship_name)
66+
# Calls method matching foreign key name on model instance
67+
define_on_resource foreign_key do
68+
@model.method(foreign_key).call
69+
end
70+
71+
# Returns instantiated related resource object or nil
72+
define_on_resource relationship_name do |options = {}|
73+
relationship = self.class._relationships[relationship_name]
74+
75+
if relationship.polymorphic?
76+
associated_model = public_send(associated_records_method_name)
77+
resource_klass = self.class.resource_for_model(associated_model) if associated_model
78+
return resource_klass.new(associated_model, @context) if resource_klass
79+
else
80+
resource_klass = relationship.resource_klass
81+
if resource_klass
82+
associated_model = public_send(associated_records_method_name)
83+
return associated_model ? resource_klass.new(associated_model, @context) : nil
84+
end
85+
end
86+
end
87+
end
88+
89+
def build_has_one(relationship, foreign_key, associated_records_method_name, relationship_name)
90+
# Returns primary key name of related resource class
91+
define_on_resource foreign_key do
92+
relationship = self.class._relationships[relationship_name]
93+
94+
record = public_send(associated_records_method_name)
95+
return nil if record.nil?
96+
record.public_send(relationship.resource_klass._primary_key)
97+
end
98+
99+
# Returns instantiated related resource object or nil
100+
define_on_resource relationship_name do |options = {}|
101+
relationship = self.class._relationships[relationship_name]
102+
103+
resource_klass = relationship.resource_klass
104+
if resource_klass
105+
associated_model = public_send(associated_records_method_name)
106+
return associated_model ? resource_klass.new(associated_model, @context) : nil
107+
end
108+
end
109+
end
110+
111+
def build_to_many(relationship, foreign_key, associated_records_method_name, relationship_name)
112+
# Returns array of primary keys of related resource classes
113+
define_on_resource foreign_key do
114+
records = public_send(associated_records_method_name)
115+
return records.collect do |record|
116+
record.public_send(relationship.resource_klass._primary_key)
117+
end
118+
end
119+
120+
# Returns array of instantiated related resource objects
121+
define_on_resource relationship_name do |options = {}|
122+
relationship = self.class._relationships[relationship_name]
123+
124+
resource_klass = relationship.resource_klass
125+
records = public_send(associated_records_method_name)
126+
127+
filters = options.fetch(:filters, {})
128+
unless filters.nil? || filters.empty?
129+
records = resource_klass.apply_filters(records, filters, options)
130+
end
131+
132+
sort_criteria = options.fetch(:sort_criteria, {})
133+
unless sort_criteria.nil? || sort_criteria.empty?
134+
order_options = relationship.resource_klass.construct_order_options(sort_criteria)
135+
records = resource_klass.apply_sort(records, order_options, @context)
136+
end
137+
138+
paginator = options[:paginator]
139+
if paginator
140+
records = resource_klass.apply_pagination(records, paginator, order_options)
141+
end
142+
143+
return records.collect do |record|
144+
if relationship.polymorphic?
145+
resource_klass = self.class.resource_for_model(record)
146+
end
147+
resource_klass.new(record, @context)
148+
end
149+
end
150+
end
151+
152+
def define_on_resource(method_name, &block)
153+
return if @resource_class.method_defined?(method_name)
154+
@resource_class.inject_method_definition(method_name, block)
155+
end
156+
end
157+
end

lib/jsonapi/resource.rb

Lines changed: 13 additions & 109 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
require 'jsonapi/callbacks'
2+
require 'jsonapi/relationship_builder'
23

34
module JSONAPI
45
class Resource
@@ -517,7 +518,7 @@ def relationship(*attrs)
517518
def has_one(*attrs)
518519
_add_relationship(Relationship::ToOne, *attrs)
519520
end
520-
521+
521522
def belongs_to(*attrs)
522523
ActiveSupport::Deprecation.warn "In #{name} you exposed a `has_one` relationship "\
523524
" using the `belongs_to` class method. We think `has_one`" \
@@ -967,119 +968,22 @@ def _add_relationship(klass, *attrs)
967968
options = attrs.extract_options!
968969
options[:parent_resource] = self
969970

970-
attrs.each do |attr|
971-
relationship_name = attr.to_sym
972-
971+
attrs.each do |relationship_name|
973972
check_reserved_relationship_name(relationship_name)
974-
975973
check_duplicate_relationship_name(relationship_name)
976974

977-
# Initialize from an ActiveRecord model's properties
978-
if _model_class && _model_class.ancestors.collect{|ancestor| ancestor.name}.include?('ActiveRecord::Base')
979-
model_association = _model_class.reflect_on_association(relationship_name)
980-
if model_association
981-
options[:class_name] ||= model_association.class_name
982-
end
983-
end
984-
985-
@_relationships[relationship_name] = relationship = klass.new(relationship_name, options)
986-
987-
associated_records_method_name = case relationship
988-
when JSONAPI::Relationship::ToOne then "record_for_#{relationship_name}"
989-
when JSONAPI::Relationship::ToMany then "records_for_#{relationship_name}"
990-
end
991-
992-
foreign_key = relationship.foreign_key
993-
994-
define_method "#{foreign_key}=" do |value|
995-
@model.method("#{foreign_key}=").call(value)
996-
end unless method_defined?("#{foreign_key}=")
997-
998-
define_method associated_records_method_name do
999-
relationship = self.class._relationships[relationship_name]
1000-
relation_name = relationship.relation_name(context: @context)
1001-
records_for(relation_name)
1002-
end unless method_defined?(associated_records_method_name)
1003-
1004-
if relationship.is_a?(JSONAPI::Relationship::ToOne)
1005-
if relationship.belongs_to?
1006-
define_method foreign_key do
1007-
@model.method(foreign_key).call
1008-
end unless method_defined?(foreign_key)
1009-
1010-
define_method relationship_name do |options = {}|
1011-
relationship = self.class._relationships[relationship_name]
1012-
1013-
if relationship.polymorphic?
1014-
associated_model = public_send(associated_records_method_name)
1015-
resource_klass = self.class.resource_for_model(associated_model) if associated_model
1016-
return resource_klass.new(associated_model, @context) if resource_klass
1017-
else
1018-
resource_klass = relationship.resource_klass
1019-
if resource_klass
1020-
associated_model = public_send(associated_records_method_name)
1021-
return associated_model ? resource_klass.new(associated_model, @context) : nil
1022-
end
1023-
end
1024-
end unless method_defined?(relationship_name)
1025-
else
1026-
define_method foreign_key do
1027-
relationship = self.class._relationships[relationship_name]
1028-
1029-
record = public_send(associated_records_method_name)
1030-
return nil if record.nil?
1031-
record.public_send(relationship.resource_klass._primary_key)
1032-
end unless method_defined?(foreign_key)
1033-
1034-
define_method relationship_name do |options = {}|
1035-
relationship = self.class._relationships[relationship_name]
1036-
1037-
resource_klass = relationship.resource_klass
1038-
if resource_klass
1039-
associated_model = public_send(associated_records_method_name)
1040-
return associated_model ? resource_klass.new(associated_model, @context) : nil
1041-
end
1042-
end unless method_defined?(relationship_name)
1043-
end
1044-
elsif relationship.is_a?(JSONAPI::Relationship::ToMany)
1045-
define_method foreign_key do
1046-
records = public_send(associated_records_method_name)
1047-
return records.collect do |record|
1048-
record.public_send(relationship.resource_klass._primary_key)
1049-
end
1050-
end unless method_defined?(foreign_key)
1051-
1052-
define_method relationship_name do |options = {}|
1053-
relationship = self.class._relationships[relationship_name]
1054-
1055-
resource_klass = relationship.resource_klass
1056-
records = public_send(associated_records_method_name)
1057-
1058-
filters = options.fetch(:filters, {})
1059-
unless filters.nil? || filters.empty?
1060-
records = resource_klass.apply_filters(records, filters, options)
1061-
end
1062-
1063-
sort_criteria = options.fetch(:sort_criteria, {})
1064-
unless sort_criteria.nil? || sort_criteria.empty?
1065-
order_options = relationship.resource_klass.construct_order_options(sort_criteria)
1066-
records = resource_klass.apply_sort(records, order_options, @context)
1067-
end
975+
JSONAPI::RelationshipBuilder.new(klass, _model_class, options)
976+
.define_relationship_methods(relationship_name.to_sym)
977+
end
978+
end
1068979

1069-
paginator = options[:paginator]
1070-
if paginator
1071-
records = resource_klass.apply_pagination(records, paginator, order_options)
1072-
end
980+
# Allows JSONAPI::RelationshipBuilder to access metaprogramming hooks
981+
def inject_method_definition(name, body)
982+
define_method(name, body)
983+
end
1073984

1074-
return records.collect do |record|
1075-
if relationship.polymorphic?
1076-
resource_klass = self.class.resource_for_model(record)
1077-
end
1078-
resource_klass.new(record, @context)
1079-
end
1080-
end unless method_defined?(relationship_name)
1081-
end
1082-
end
985+
def register_relationship(name, relationship_object)
986+
@_relationships[name] = relationship_object
1083987
end
1084988

1085989
private

0 commit comments

Comments
 (0)