jsonapi-resources 0.8.3 → 0.9.0.beta1

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.
@@ -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