jsonapi-resources 0.8.3 → 0.9.0.beta1

Sign up to get free protection for your applications and to get access to all the features.
@@ -36,6 +36,10 @@ module JSONAPI
36
36
  _model.public_send(self.class._primary_key)
37
37
  end
38
38
 
39
+ def cache_id
40
+ [id, _model.public_send(self.class._cache_field)]
41
+ end
42
+
39
43
  def is_new?
40
44
  id.nil?
41
45
  end
@@ -165,6 +169,11 @@ module JSONAPI
165
169
  {}
166
170
  end
167
171
 
172
+ def preloaded_fragments
173
+ # A hash of hashes
174
+ @preloaded_fragments ||= Hash.new
175
+ end
176
+
168
177
  private
169
178
 
170
179
  def save
@@ -315,7 +324,7 @@ module JSONAPI
315
324
  relationship = self.class._relationships[relationship_type.to_sym]
316
325
 
317
326
  _model.public_send("#{relationship.foreign_key}=", key_value)
318
- _model.public_send("#{relationship.polymorphic_type}=", key_type.to_s.classify)
327
+ _model.public_send("#{relationship.polymorphic_type}=", _model_class_name(key_type))
319
328
 
320
329
  @save_needed = true
321
330
 
@@ -395,10 +404,17 @@ module JSONAPI
395
404
  :completed
396
405
  end
397
406
 
407
+ def _model_class_name(key_type)
408
+ type_class_name = key_type.to_s.classify
409
+ resource = self.class.resource_for(type_class_name)
410
+ resource ? resource._model_name.to_s : type_class_name
411
+ end
412
+
398
413
  class << self
399
414
  def inherited(subclass)
400
415
  subclass.abstract(false)
401
416
  subclass.immutable(false)
417
+ subclass.caching(false)
402
418
  subclass._attributes = (_attributes || {}).dup
403
419
  subclass._model_hints = (_model_hints || {}).dup
404
420
 
@@ -417,9 +433,7 @@ module JSONAPI
417
433
  type = subclass.name.demodulize.sub(/Resource$/, '').underscore
418
434
  subclass._type = type.pluralize.to_sym
419
435
 
420
- unless subclass._attributes[:id]
421
- subclass.attribute :id, format: :id
422
- end
436
+ subclass.attribute :id, format: :id
423
437
 
424
438
  check_reserved_resource_name(subclass._type, subclass.name)
425
439
  end
@@ -453,7 +467,8 @@ module JSONAPI
453
467
  end
454
468
  end
455
469
 
456
- attr_accessor :_attributes, :_relationships, :_allowed_filters, :_type, :_paginator, :_model_hints
470
+ attr_accessor :_attributes, :_relationships, :_type, :_model_hints
471
+ attr_writer :_allowed_filters, :_paginator
457
472
 
458
473
  def create(context)
459
474
  new(create_model, context)
@@ -559,20 +574,9 @@ module JSONAPI
559
574
  @_primary_key = key.to_sym
560
575
  end
561
576
 
562
- # TODO: remove this after the createable_fields and updateable_fields are phased out
563
- # :nocov:
564
- def method_missing(method, *args)
565
- if method.to_s.match /createable_fields/
566
- ActiveSupport::Deprecation.warn('`createable_fields` is deprecated, please use `creatable_fields` instead')
567
- creatable_fields(*args)
568
- elsif method.to_s.match /updateable_fields/
569
- ActiveSupport::Deprecation.warn('`updateable_fields` is deprecated, please use `updatable_fields` instead')
570
- updatable_fields(*args)
571
- else
572
- super
573
- end
577
+ def cache_field(field)
578
+ @_cache_field = field.to_sym
574
579
  end
575
- # :nocov:
576
580
 
577
581
  # Override in your resource to filter the updatable keys
578
582
  def updatable_fields(_context = nil)
@@ -581,7 +585,7 @@ module JSONAPI
581
585
 
582
586
  # Override in your resource to filter the creatable keys
583
587
  def creatable_fields(_context = nil)
584
- _updatable_relationships | _attributes.keys - [:id]
588
+ _updatable_relationships | _attributes.keys
585
589
  end
586
590
 
587
591
  # Override in your resource to filter the sortable keys
@@ -729,19 +733,8 @@ module JSONAPI
729
733
  count_records(filter_records(filters, options))
730
734
  end
731
735
 
732
- # Override this method if you have more complex requirements than this basic find method provides
733
736
  def find(filters, options = {})
734
- context = options[:context]
735
-
736
- records = filter_records(filters, options)
737
-
738
- sort_criteria = options.fetch(:sort_criteria) { [] }
739
- order_options = construct_order_options(sort_criteria)
740
- records = sort_records(records, order_options, context)
741
-
742
- records = apply_pagination(records, options[:paginator], order_options)
743
-
744
- resources_for(records, context)
737
+ resources_for(find_records(filters, options), options[:context])
745
738
  end
746
739
 
747
740
  def resources_for(records, context)
@@ -761,17 +754,39 @@ module JSONAPI
761
754
  end
762
755
  end
763
756
 
757
+ def find_serialized_with_caching(filters_or_source, serializer, options = {})
758
+ if filters_or_source.is_a?(ActiveRecord::Relation)
759
+ records = filters_or_source
760
+ elsif _model_class.respond_to?(:all) && _model_class.respond_to?(:arel_table)
761
+ records = find_records(filters_or_source, options.except(:include_directives))
762
+ else
763
+ records = find(filters_or_source, options)
764
+ end
765
+ cached_resources_for(records, serializer, options)
766
+ end
767
+
764
768
  def find_by_key(key, options = {})
765
769
  context = options[:context]
766
- records = records(options)
767
- records = apply_includes(records, options)
768
- model = records.where({_primary_key => key}).first
770
+ records = find_records({_primary_key => key}, options.except(:paginator, :sort_criteria))
771
+ model = records.first
769
772
  fail JSONAPI::Exceptions::RecordNotFound.new(key) if model.nil?
770
773
  self.resource_for_model(model).new(model, context)
771
774
  end
772
775
 
776
+ def find_by_key_serialized_with_caching(key, serializer, options = {})
777
+ if _model_class.respond_to?(:all) && _model_class.respond_to?(:arel_table)
778
+ results = find_serialized_with_caching({_primary_key => key}, serializer, options)
779
+ result = results.first
780
+ fail JSONAPI::Exceptions::RecordNotFound.new(key) if result.nil?
781
+ return result
782
+ else
783
+ resource = find_by_key(key, options)
784
+ return cached_resources_for([resource], serializer, options).first
785
+ end
786
+ end
787
+
773
788
  # Override this method if you want to customize the relation for
774
- # finder methods (find, find_by_key)
789
+ # finder methods (find, find_by_key, find_serialized_with_caching)
775
790
  def records(_options = {})
776
791
  _model_class.all
777
792
  end
@@ -882,13 +897,24 @@ module JSONAPI
882
897
  end
883
898
 
884
899
  def _model_name
885
- _abstract ? '' : @_model_name ||= name.demodulize.sub(/Resource$/, '')
900
+ if _abstract
901
+ return ''
902
+ else
903
+ return @_model_name if defined?(@_model_name)
904
+ class_name = self.name
905
+ return '' if class_name.nil?
906
+ return @_model_name = class_name.demodulize.sub(/Resource$/, '')
907
+ end
886
908
  end
887
909
 
888
910
  def _primary_key
889
911
  @_primary_key ||= _model_class.respond_to?(:primary_key) ? _model_class.primary_key : :id
890
912
  end
891
913
 
914
+ def _cache_field
915
+ @_cache_field ||= JSONAPI.configuration.default_resource_cache_field
916
+ end
917
+
892
918
  def _table_name
893
919
  @_table_name ||= _model_class.respond_to?(:table_name) ? _model_class.table_name : _model_name.tableize
894
920
  end
@@ -898,7 +924,7 @@ module JSONAPI
898
924
  end
899
925
 
900
926
  def _allowed_filters
901
- !@_allowed_filters.nil? ? @_allowed_filters : { id: {} }
927
+ defined?(@_allowed_filters) ? @_allowed_filters : { id: {} }
902
928
  end
903
929
 
904
930
  def _paginator
@@ -929,10 +955,27 @@ module JSONAPI
929
955
  !@immutable
930
956
  end
931
957
 
958
+ def caching(val = true)
959
+ @caching = val
960
+ end
961
+
962
+ def _caching
963
+ @caching
964
+ end
965
+
966
+ def caching?
967
+ @caching && !JSONAPI.configuration.resource_cache.nil?
968
+ end
969
+
970
+ def attribute_caching_context(context)
971
+ nil
972
+ end
973
+
932
974
  def _model_class
933
975
  return nil if _abstract
934
976
 
935
- return @model if @model
977
+ return @model if defined?(@model)
978
+ return nil if self.name.to_s.blank? && _model_name.to_s.blank?
936
979
  @model = _model_name.to_s.safe_constantize
937
980
  warn "[MODEL NOT FOUND] Model could not be found for #{self.name}. If this a base Resource declare it as abstract." if @model.nil?
938
981
  @model
@@ -989,6 +1032,36 @@ module JSONAPI
989
1032
 
990
1033
  private
991
1034
 
1035
+ def cached_resources_for(records, serializer, options)
1036
+ if records.is_a?(Array) && records.all?{|rec| rec.is_a?(JSONAPI::Resource)}
1037
+ resources = records.map{|r| [r.id, r] }.to_h
1038
+ elsif self.caching?
1039
+ t = _model_class.arel_table
1040
+ cache_ids = pluck_arel_attributes(records, t[_primary_key], t[_cache_field])
1041
+ resources = CachedResourceFragment.fetch_fragments(self, serializer, options[:context], cache_ids)
1042
+ else
1043
+ resources = resources_for(records, options).map{|r| [r.id, r] }.to_h
1044
+ end
1045
+
1046
+ preload_included_fragments(resources, records, serializer, options)
1047
+
1048
+ resources.values
1049
+ end
1050
+
1051
+ def find_records(filters, options = {})
1052
+ context = options[:context]
1053
+
1054
+ records = filter_records(filters, options)
1055
+
1056
+ sort_criteria = options.fetch(:sort_criteria) { [] }
1057
+ order_options = construct_order_options(sort_criteria)
1058
+ records = sort_records(records, order_options, context)
1059
+
1060
+ records = apply_pagination(records, options[:paginator], order_options)
1061
+
1062
+ records
1063
+ end
1064
+
992
1065
  def check_reserved_resource_name(type, name)
993
1066
  if [:ids, :types, :hrefs, :links].include?(type)
994
1067
  warn "[NAME COLLISION] `#{name}` is a reserved resource name."
@@ -1021,6 +1094,129 @@ module JSONAPI
1021
1094
  warn "[DUPLICATE ATTRIBUTE] `#{name}` has already been defined in #{_resource_name_from_type(_type)}."
1022
1095
  end
1023
1096
  end
1097
+
1098
+ def preload_included_fragments(resources, records, serializer, options)
1099
+ return if resources.empty?
1100
+ res_ids = resources.keys
1101
+
1102
+ include_directives = options[:include_directives]
1103
+ return unless include_directives
1104
+
1105
+ relevant_options = options.except(:include_directives, :order, :paginator)
1106
+ context = options[:context]
1107
+
1108
+ # For each association, including indirect associations, find the target record ids.
1109
+ # Even if a target class doesn't have caching enabled, we still have to look up
1110
+ # and match the target ids here, because we can't use ActiveRecord#includes.
1111
+ #
1112
+ # Note that `paths` returns partial paths before complete paths, so e.g. the partial
1113
+ # fragments for posts.comments will exist before we start working with posts.comments.author
1114
+ target_resources = {}
1115
+ include_directives.paths.each do |path|
1116
+ # If path is [:posts, :comments, :author], then...
1117
+ pluck_attrs = [] # ...will be [posts.id, comments.id, authors.id, authors.updated_at]
1118
+ pluck_attrs << self._model_class.arel_table[self._primary_key]
1119
+
1120
+ relation = records
1121
+ .except(:limit, :offset, :order)
1122
+ .where({_primary_key => res_ids})
1123
+
1124
+ # These are updated as we iterate through the association path; afterwards they will
1125
+ # refer to the final resource on the path, i.e. the actual resource to find in the cache.
1126
+ # So e.g. if path is [:posts, :comments, :author], then after iteration...
1127
+ parent_klass = nil # Comment
1128
+ klass = self # Person
1129
+ relationship = nil # JSONAPI::Relationship::ToOne for CommentResource.author
1130
+ table = nil # people
1131
+ assocs_path = [] # [ :posts, :approved_comments, :author ]
1132
+ ar_hash = nil # { :posts => { :approved_comments => :author } }
1133
+
1134
+ # For each step on the path, figure out what the actual table name/alias in the join
1135
+ # will be, and include the primary key of that table in our list of fields to select
1136
+ path.each do |elem|
1137
+ relationship = klass._relationships[elem]
1138
+ assocs_path << relationship.relation_name(options).to_sym
1139
+ # Converts [:a, :b, :c] to Rails-style { :a => { :b => :c }}
1140
+ ar_hash = assocs_path.reverse.reduce{|memo, step| { step => memo } }
1141
+ # We can't just look up the table name from the resource class, because Arel could
1142
+ # have used a table alias if the relation includes a self-reference.
1143
+ join_source = relation.joins(ar_hash).arel.source.right.reverse.find do |arel_node|
1144
+ arel_node.is_a?(Arel::Nodes::InnerJoin)
1145
+ end
1146
+ table = join_source.left
1147
+ parent_klass = klass
1148
+ klass = relationship.resource_klass
1149
+ pluck_attrs << table[klass._primary_key]
1150
+ end
1151
+
1152
+ # Pre-fill empty hashes for each resource up to the end of the path.
1153
+ # This allows us to later distinguish between a preload that returned nothing
1154
+ # vs. a preload that never ran.
1155
+ prefilling_resources = resources.values
1156
+ path.each do |rel_name|
1157
+ rel_name = serializer.key_formatter.format(rel_name)
1158
+ prefilling_resources.map! do |res|
1159
+ res.preloaded_fragments[rel_name] ||= {}
1160
+ res.preloaded_fragments[rel_name].values
1161
+ end
1162
+ prefilling_resources.flatten!(1)
1163
+ end
1164
+
1165
+ pluck_attrs << table[klass._cache_field] if klass.caching?
1166
+ relation = relation.joins(ar_hash)
1167
+ if relationship.is_a?(JSONAPI::Relationship::ToMany)
1168
+ # Rails doesn't include order clauses in `joins`, so we have to add that manually here.
1169
+ # FIXME Should find a better way to reflect on relationship ordering. :-(
1170
+ relation = relation.order(parent_klass._model_class.new.send(assocs_path.last).arel.orders)
1171
+ end
1172
+
1173
+ # [[post id, comment id, author id, author updated_at], ...]
1174
+ id_rows = pluck_arel_attributes(relation.joins(ar_hash), *pluck_attrs)
1175
+
1176
+ target_resources[klass.name] ||= {}
1177
+
1178
+ if klass.caching?
1179
+ sub_cache_ids = id_rows
1180
+ .map{|row| row.last(2) }
1181
+ .reject{|row| target_resources[klass.name].has_key?(row.first) }
1182
+ .uniq
1183
+ target_resources[klass.name].merge! CachedResourceFragment.fetch_fragments(
1184
+ klass, serializer, context, sub_cache_ids
1185
+ )
1186
+ else
1187
+ sub_res_ids = id_rows
1188
+ .map(&:last)
1189
+ .reject{|id| target_resources[klass.name].has_key?(id) }
1190
+ .uniq
1191
+ found = klass.find({klass._primary_key => sub_res_ids}, relevant_options)
1192
+ target_resources[klass.name].merge! found.map{|r| [r.id, r] }.to_h
1193
+ end
1194
+
1195
+ id_rows.each do |row|
1196
+ res = resources[row.first]
1197
+ path.each_with_index do |rel_name, index|
1198
+ rel_name = serializer.key_formatter.format(rel_name)
1199
+ rel_id = row[index+1]
1200
+ assoc_rels = res.preloaded_fragments[rel_name]
1201
+ if index == path.length - 1
1202
+ assoc_rels[rel_id] = target_resources[klass.name].fetch(rel_id)
1203
+ else
1204
+ res = assoc_rels[rel_id]
1205
+ end
1206
+ end
1207
+ end
1208
+ end
1209
+ end
1210
+
1211
+ def pluck_arel_attributes(relation, *attrs)
1212
+ conn = relation.connection
1213
+ quoted_attrs = attrs.map do |attr|
1214
+ quoted_table = conn.quote_table_name(attr.relation.table_alias || attr.relation.name)
1215
+ quoted_column = conn.quote_column_name(attr.name)
1216
+ "#{quoted_table}.#{quoted_column}"
1217
+ end
1218
+ relation.pluck(*quoted_attrs)
1219
+ end
1024
1220
  end
1025
1221
  end
1026
1222
  end
@@ -1,7 +1,9 @@
1
1
  module JSONAPI
2
2
  class ResourceSerializer
3
3
 
4
- attr_reader :link_builder, :key_formatter, :serialization_options, :primary_class_name
4
+ attr_reader :link_builder, :key_formatter, :serialization_options, :primary_class_name,
5
+ :fields, :include_directives, :always_include_to_one_linkage_data,
6
+ :always_include_to_many_linkage_data
5
7
 
6
8
  # initialize
7
9
  # Options can include
@@ -13,7 +15,7 @@ module JSONAPI
13
15
  # relationship ids in the links section for a resource. Fields are global for a resource type.
14
16
  # Example: { people: [:id, :email, :comments], posts: [:id, :title, :author], comments: [:id, :body, :post]}
15
17
  # key_formatter: KeyFormatter instance to override the default configuration
16
- # serializer_options: additional options that will be passed to resource meta and links lambdas
18
+ # serialization_options: additional options that will be passed to resource meta and links lambdas
17
19
 
18
20
  def initialize(primary_resource_klass, options = {})
19
21
  @primary_resource_klass = primary_resource_klass
@@ -21,6 +23,7 @@ module JSONAPI
21
23
  @fields = options.fetch(:fields, {})
22
24
  @include = options.fetch(:include, [])
23
25
  @include_directives = options[:include_directives]
26
+ @include_directives ||= JSONAPI::IncludeDirectives.new(@primary_resource_klass, @include)
24
27
  @key_formatter = options.fetch(:key_formatter, JSONAPI.configuration.key_formatter)
25
28
  @id_formatter = ValueFormatter.value_formatter_for(:id)
26
29
  @link_builder = generate_link_builder(primary_resource_klass, options)
@@ -33,16 +36,19 @@ module JSONAPI
33
36
  # Warning: This makes ResourceSerializer non-thread-safe. That's not a problem with the
34
37
  # request-specific way it's currently used, though.
35
38
  @value_formatter_type_cache = NaiveCache.new{|arg| ValueFormatter.value_formatter_for(arg) }
39
+
40
+ @_config_keys = {}
41
+ @_supplying_attribute_fields = {}
42
+ @_supplying_relationship_fields = {}
36
43
  end
37
44
 
38
45
  # Converts a single resource, or an array of resources to a hash, conforming to the JSONAPI structure
39
46
  def serialize_to_hash(source)
40
- @top_level_sources = Set.new([source].flatten.compact.map {|s| top_level_source_key(s) })
47
+ @top_level_sources = Set.new([source].flatten(1).compact.map {|s| top_level_source_key(s) })
41
48
 
42
49
  is_resource_collection = source.respond_to?(:to_ary)
43
50
 
44
51
  @included_objects = {}
45
- @include_directives ||= JSONAPI::IncludeDirectives.new(@primary_resource_klass, @include)
46
52
 
47
53
  process_primary(source, @include_directives.include_directives)
48
54
 
@@ -92,67 +98,120 @@ module JSONAPI
92
98
  @value_formatter_type_cache.get(format).format(value)
93
99
  end
94
100
 
95
- private
96
-
97
- # Process the primary source object(s). This will then serialize associated object recursively based on the
98
- # requested includes. Fields are controlled fields option for each resource type, such
99
- # as fields: { people: [:id, :email, :comments], posts: [:id, :title, :author], comments: [:id, :body, :post]}
100
- # The fields options controls both fields and included links references.
101
- def process_primary(source, include_directives)
102
- if source.respond_to?(:to_ary)
103
- source.each { |resource| process_primary(resource, include_directives) }
104
- else
105
- return {} if source.nil?
106
-
107
- resource = source
108
- id = resource.id
109
- add_included_object(id, object_hash(source, include_directives), true)
101
+ def config_key(resource_klass)
102
+ @_config_keys.fetch resource_klass do
103
+ desc = self.config_description(resource_klass).map(&:inspect).join(",")
104
+ key = JSONAPI.configuration.resource_cache_digest_function.call(desc)
105
+ @_config_keys[resource_klass] = "SRLZ-#{key}"
110
106
  end
111
107
  end
112
108
 
109
+ def config_description(resource_klass)
110
+ {
111
+ class_name: self.class.name,
112
+ seriserialization_options: serialization_options.sort.map(&:as_json),
113
+ supplying_attribute_fields: supplying_attribute_fields(resource_klass).sort,
114
+ supplying_relationship_fields: supplying_relationship_fields(resource_klass).sort,
115
+ link_builder_base_url: link_builder.base_url,
116
+ route_formatter_class: link_builder.route_formatter.uncached.class.name,
117
+ key_formatter_class: key_formatter.uncached.class.name,
118
+ always_include_to_one_linkage_data: always_include_to_one_linkage_data,
119
+ always_include_to_many_linkage_data: always_include_to_many_linkage_data
120
+ }
121
+ end
122
+
113
123
  # Returns a serialized hash for the source model
114
- def object_hash(source, include_directives)
124
+ def object_hash(source, include_directives = {})
115
125
  obj_hash = {}
116
126
 
117
- id_format = source.class._attribute_options(:id)[:format]
118
- # protect against ids that were declared as an attribute, but did not have a format set.
119
- id_format = 'id' if id_format == :default
120
- obj_hash['id'] = format_value(source.id, id_format)
127
+ if source.is_a?(JSONAPI::CachedResourceFragment)
128
+ obj_hash['id'] = source.id
129
+ obj_hash['type'] = source.type
121
130
 
122
- obj_hash['type'] = format_key(source.class._type.to_s)
131
+ obj_hash['links'] = source.links_json if source.links_json
132
+ obj_hash['attributes'] = source.attributes_json if source.attributes_json
123
133
 
124
- links = links_hash(source)
125
- obj_hash['links'] = links unless links.empty?
134
+ relationships = cached_relationships_hash(source, include_directives)
135
+ obj_hash['relationships'] = relationships unless relationships.empty?
126
136
 
127
- attributes = attributes_hash(source)
128
- obj_hash['attributes'] = attributes unless attributes.empty?
137
+ obj_hash['meta'] = source.meta_json if source.meta_json
138
+ else
139
+ fetchable_fields = Set.new(source.fetchable_fields)
129
140
 
130
- relationships = relationships_hash(source, include_directives)
131
- obj_hash['relationships'] = relationships unless relationships.nil? || relationships.empty?
141
+ # TODO Should this maybe be using @id_formatter instead, for consistency?
142
+ id_format = source.class._attribute_options(:id)[:format]
143
+ # protect against ids that were declared as an attribute, but did not have a format set.
144
+ id_format = 'id' if id_format == :default
145
+ obj_hash['id'] = format_value(source.id, id_format)
132
146
 
133
- meta = meta_hash(source)
134
- obj_hash['meta'] = meta unless meta.empty?
147
+ obj_hash['type'] = format_key(source.class._type.to_s)
148
+
149
+ links = links_hash(source)
150
+ obj_hash['links'] = links unless links.empty?
151
+
152
+ attributes = attributes_hash(source, fetchable_fields)
153
+ obj_hash['attributes'] = attributes unless attributes.empty?
154
+
155
+ relationships = relationships_hash(source, fetchable_fields, include_directives)
156
+ obj_hash['relationships'] = relationships unless relationships.nil? || relationships.empty?
157
+
158
+ meta = meta_hash(source)
159
+ obj_hash['meta'] = meta unless meta.empty?
160
+ end
135
161
 
136
162
  obj_hash
137
163
  end
138
164
 
139
- def requested_fields(klass)
140
- return if @fields.nil? || @fields.empty?
141
- if @fields[klass._type]
142
- @fields[klass._type]
143
- elsif klass.superclass != JSONAPI::Resource
144
- requested_fields(klass.superclass)
165
+ private
166
+
167
+ # Process the primary source object(s). This will then serialize associated object recursively based on the
168
+ # requested includes. Fields are controlled fields option for each resource type, such
169
+ # as fields: { people: [:id, :email, :comments], posts: [:id, :title, :author], comments: [:id, :body, :post]}
170
+ # The fields options controls both fields and included links references.
171
+ def process_primary(source, include_directives)
172
+ if source.respond_to?(:to_ary)
173
+ source.each { |resource| process_primary(resource, include_directives) }
174
+ else
175
+ return {} if source.nil?
176
+ add_resource(source, include_directives, true)
145
177
  end
146
178
  end
147
179
 
148
- def attributes_hash(source)
149
- requested = requested_fields(source.class)
150
- fields = source.fetchable_fields & source.class._attributes.keys.to_a
151
- fields = requested & fields unless requested.nil?
180
+ def supplying_attribute_fields(resource_klass)
181
+ @_supplying_attribute_fields.fetch resource_klass do
182
+ attrs = Set.new(resource_klass._attributes.keys.map(&:to_sym))
183
+ cur = resource_klass
184
+ while cur != JSONAPI::Resource
185
+ if @fields.has_key?(cur._type)
186
+ attrs &= @fields[cur._type]
187
+ break
188
+ end
189
+ cur = cur.superclass
190
+ end
191
+ @_supplying_attribute_fields[resource_klass] = attrs
192
+ end
193
+ end
152
194
 
195
+ def supplying_relationship_fields(resource_klass)
196
+ @_supplying_relationship_fields.fetch resource_klass do
197
+ relationships = Set.new(resource_klass._relationships.keys.map(&:to_sym))
198
+ cur = resource_klass
199
+ while cur != JSONAPI::Resource
200
+ if @fields.has_key?(cur._type)
201
+ relationships &= @fields[cur._type]
202
+ break
203
+ end
204
+ cur = cur.superclass
205
+ end
206
+ @_supplying_relationship_fields[resource_klass] = relationships
207
+ end
208
+ end
209
+
210
+ def attributes_hash(source, fetchable_fields)
211
+ fields = fetchable_fields & supplying_attribute_fields(source.class)
153
212
  fields.each_with_object({}) do |name, hash|
154
- format = source.class._attribute_options(name)[:format]
155
213
  unless name == :id
214
+ format = source.class._attribute_options(name)[:format]
156
215
  hash[format_key(name)] = format_value(source.public_send(name), format)
157
216
  end
158
217
  end
@@ -182,59 +241,101 @@ module JSONAPI
182
241
  end
183
242
 
184
243
  def top_level_source_key(source)
185
- "#{source.class}_#{source.id}"
244
+ case source
245
+ when CachedResourceFragment then "#{source.resource_klass}_#{source.id}"
246
+ when Resource then "#{source.class}_#{@id_formatter.format(source.id)}"
247
+ else raise "Unknown source type #{source.inspect}"
248
+ end
186
249
  end
187
250
 
188
251
  def self_referential_and_already_in_source(resource)
189
252
  resource && @top_level_sources.include?(top_level_source_key(resource))
190
253
  end
191
254
 
192
- def relationships_hash(source, include_directives)
193
- relationships = source.class._relationships
194
- requested = requested_fields(source.class)
195
- fields = relationships.keys
196
- fields = requested & fields unless requested.nil?
255
+ def relationships_hash(source, fetchable_fields, include_directives = {})
256
+ if source.is_a?(CachedResourceFragment)
257
+ return cached_relationships_hash(source, include_directives)
258
+ end
259
+
260
+ include_directives[:include_related] ||= {}
261
+
262
+ relationships = source.class._relationships.select{|k,v| fetchable_fields.include?(k) }
263
+ field_set = supplying_relationship_fields(source.class) & relationships.keys
264
+
265
+ relationships.each_with_object({}) do |(name, relationship), hash|
266
+ ia = include_directives[:include_related][name]
267
+ include_linkage = ia && ia[:include]
268
+ include_linked_children = ia && !ia[:include_related].empty?
197
269
 
198
- field_set = Set.new(fields)
270
+ if field_set.include?(name)
271
+ hash[format_key(name)] = link_object(source, relationship, include_linkage)
272
+ end
199
273
 
200
- included_relationships = source.fetchable_fields & relationships.keys
274
+ # If the object has been serialized once it will be in the related objects list,
275
+ # but it's possible all children won't have been captured. So we must still go
276
+ # through the relationships.
277
+ if include_linkage || include_linked_children
278
+ resources = if source.preloaded_fragments.has_key?(format_key(name))
279
+ source.preloaded_fragments[format_key(name)].values
280
+ else
281
+ [source.public_send(name)].flatten(1).compact
282
+ end
283
+ resources.each do |resource|
284
+ next if self_referential_and_already_in_source(resource)
285
+ id = resource.id
286
+ relationships_only = already_serialized?(relationship.type, id)
287
+ if include_linkage && !relationships_only
288
+ add_resource(resource, ia)
289
+ elsif include_linked_children || relationships_only
290
+ relationships_hash(resource, fetchable_fields, ia)
291
+ end
292
+ end
293
+ end
294
+ end
295
+ end
201
296
 
202
- data = {}
297
+ def cached_relationships_hash(source, include_directives)
298
+ h = source.relationships || {}
299
+ return h unless include_directives.has_key?(:include_related)
203
300
 
204
- relationships.each_with_object(data) do |(name, relationship), hash|
205
- if included_relationships.include? name
206
- ia = include_directives[:include_related][name]
301
+ relationships = source.resource_klass._relationships.select{|k,v| source.fetchable_fields.include?(k) }
207
302
 
208
- include_linkage = ia && ia[:include]
209
- include_linked_children = ia && !ia[:include_related].empty?
210
- resources = (include_linkage || include_linked_children) && [source.public_send(name)].flatten.compact
303
+ relationships.each do |rel_name, relationship|
304
+ key = @key_formatter.format(rel_name)
305
+ to_many = relationship.is_a? JSONAPI::Relationship::ToMany
211
306
 
212
- if field_set.include?(name)
213
- hash[format_key(name)] = link_object(source, relationship, include_linkage)
307
+ ia = include_directives[:include_related][rel_name]
308
+ if ia
309
+ if h.has_key?(key)
310
+ h[key][:data] = to_many ? [] : nil
214
311
  end
215
312
 
216
- # If the object has been serialized once it will be in the related objects list,
217
- # but it's possible all children won't have been captured. So we must still go
218
- # through the relationships.
219
- if include_linkage || include_linked_children
220
- resources.each do |resource|
221
- next if self_referential_and_already_in_source(resource)
222
- id = resource.id
223
- type = resource.class.resource_for_model(resource._model)
224
- relationships_only = already_serialized?(type, id)
225
- if include_linkage && !relationships_only
226
- add_included_object(id, object_hash(resource, ia))
227
- elsif include_linked_children || relationships_only
228
- relationships_hash(resource, ia)
313
+ source.preloaded_fragments[key].each do |id, f|
314
+ add_resource(f, ia)
315
+
316
+ if h.has_key?(key)
317
+ # The hash already has everything we need except the :data field
318
+ data = {
319
+ type: format_key(f.is_a?(Resource) ? f.class._type : f.type),
320
+ id: @id_formatter.format(id)
321
+ }
322
+
323
+ if to_many
324
+ h[key][:data] << data
325
+ else
326
+ h[key][:data] = data
229
327
  end
230
328
  end
231
329
  end
232
330
  end
233
331
  end
332
+
333
+ return h
234
334
  end
235
335
 
236
336
  def already_serialized?(type, id)
237
337
  type = format_key(type)
338
+ id = @id_formatter.format(id)
238
339
  @included_objects.key?(type) && @included_objects[type].key?(id)
239
340
  end
240
341
 
@@ -247,8 +348,10 @@ module JSONAPI
247
348
  end
248
349
 
249
350
  def to_one_linkage(source, relationship)
250
- return unless linkage_id = foreign_key_value(source, relationship)
251
- return unless linkage_type = format_key(relationship.type_for_source(source))
351
+ linkage_id = foreign_key_value(source, relationship)
352
+ linkage_type = format_key(relationship.type_for_source(source))
353
+ return unless linkage_id.present? && linkage_type.present?
354
+
252
355
  {
253
356
  type: linkage_type,
254
357
  id: linkage_id,
@@ -257,7 +360,27 @@ module JSONAPI
257
360
 
258
361
  def to_many_linkage(source, relationship)
259
362
  linkage = []
260
- linkage_types_and_values = foreign_key_types_and_values(source, relationship)
363
+ linkage_types_and_values = if source.preloaded_fragments.has_key?(format_key(relationship.name))
364
+ source.preloaded_fragments[format_key(relationship.name)].map do |_, resource|
365
+ [relationship.type, resource.id]
366
+ end
367
+ elsif relationship.polymorphic?
368
+ assoc = source._model.public_send(relationship.name)
369
+ # Avoid hitting the database again for values already pre-loaded
370
+ if assoc.respond_to?(:loaded?) and assoc.loaded?
371
+ assoc.map do |obj|
372
+ [obj.type.underscore.pluralize, obj.id]
373
+ end
374
+ else
375
+ assoc.pluck(:type, :id).map do |type, id|
376
+ [type.underscore.pluralize, id]
377
+ end
378
+ end
379
+ else
380
+ source.public_send(relationship.name).map do |value|
381
+ [relationship.type, value.id]
382
+ end
383
+ end
261
384
 
262
385
  linkage_types_and_values.each do |type, value|
263
386
  if type && value
@@ -297,18 +420,18 @@ module JSONAPI
297
420
 
298
421
  # Extracts the foreign key value for a to_one relationship.
299
422
  def foreign_key_value(source, relationship)
300
- # If you have direct access to the underlying id, you don't have to load the relationship
301
- # which can save quite a lot of time when loading a lot of data.
302
- # This does not apply to e.g. has_one :through relationships.
303
- if source._model.respond_to?("#{relationship.name}_id")
304
- related_resource_id = source._model.public_send("#{relationship.name}_id")
305
- return nil unless related_resource_id
306
- @id_formatter.format(related_resource_id)
423
+ related_resource_id = if source.preloaded_fragments.has_key?(format_key(relationship.name))
424
+ source.preloaded_fragments[format_key(relationship.name)].values.first.try(:id)
425
+ elsif source.respond_to?("#{relationship.name}_id")
426
+ # If you have direct access to the underlying id, you don't have to load the relationship
427
+ # which can save quite a lot of time when loading a lot of data.
428
+ # This does not apply to e.g. has_one :through relationships.
429
+ source.public_send("#{relationship.name}_id")
307
430
  else
308
- related_resource = source.public_send(relationship.name)
309
- return nil unless related_resource
310
- @id_formatter.format(related_resource.id)
431
+ source.public_send(relationship.name).try(:id)
311
432
  end
433
+ return nil unless related_resource_id
434
+ @id_formatter.format(related_resource_id)
312
435
  end
313
436
 
314
437
  def foreign_key_types_and_values(source, relationship)
@@ -339,17 +462,28 @@ module JSONAPI
339
462
  @included_objects[type][id][:primary] = true
340
463
  end
341
464
 
342
- # Collects the hashes for all objects processed by the serializer
343
- def add_included_object(id, object_hash, primary = false)
344
- type = object_hash['type']
465
+ def add_resource(source, include_directives, primary = false)
466
+ type = source.is_a?(JSONAPI::CachedResourceFragment) ? source.type : source.class._type
467
+ id = source.id
345
468
 
346
- @included_objects[type] = {} unless @included_objects.key?(type)
469
+ @included_objects[type] ||= {}
470
+ existing = @included_objects[type][id]
347
471
 
348
- if already_serialized?(type, id)
349
- @included_objects[type][id][:object_hash].deep_merge!(object_hash)
350
- set_primary(type, id) if primary
472
+ if existing.nil?
473
+ obj_hash = object_hash(source, include_directives)
474
+ @included_objects[type][id] = {
475
+ primary: primary,
476
+ object_hash: obj_hash,
477
+ includes: Set.new(include_directives[:include_related].keys)
478
+ }
351
479
  else
352
- @included_objects[type].store(id, primary: primary, object_hash: object_hash)
480
+ include_related = Set.new(include_directives[:include_related].keys)
481
+ unless existing[:includes].superset?(include_related)
482
+ obj_hash = object_hash(source, include_directives)
483
+ @included_objects[type][id][:object_hash].deep_merge!(obj_hash)
484
+ @included_objects[type][id][:includes].add(include_related)
485
+ @included_objects[type][id][:primary] = existing[:primary] | primary
486
+ end
353
487
  end
354
488
  end
355
489