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.
- checksums.yaml +4 -4
- data/README.md +2124 -8
- data/lib/jsonapi-resources.rb +2 -0
- data/lib/jsonapi/acts_as_resource_controller.rb +70 -29
- data/lib/jsonapi/cached_resource_fragment.rb +119 -0
- data/lib/jsonapi/compiled_json.rb +36 -0
- data/lib/jsonapi/configuration.rb +54 -4
- data/lib/jsonapi/error_codes.rb +2 -2
- data/lib/jsonapi/exceptions.rb +19 -13
- data/lib/jsonapi/formatter.rb +15 -1
- data/lib/jsonapi/include_directives.rb +23 -3
- data/lib/jsonapi/processor.rb +69 -27
- data/lib/jsonapi/relationship_builder.rb +23 -21
- data/lib/jsonapi/request_parser.rb +27 -72
- data/lib/jsonapi/resource.rb +234 -38
- data/lib/jsonapi/resource_serializer.rb +229 -95
- data/lib/jsonapi/resources/version.rb +1 -1
- data/lib/jsonapi/response_document.rb +9 -20
- metadata +25 -9
data/lib/jsonapi/resource.rb
CHANGED
@@ -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
|
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
|
-
|
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, :
|
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
|
-
|
563
|
-
|
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
|
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
|
-
|
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 =
|
767
|
-
|
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
|
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
|
-
|
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
|
-
#
|
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
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
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
|
-
|
118
|
-
|
119
|
-
|
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
|
-
|
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
|
-
|
125
|
-
|
134
|
+
relationships = cached_relationships_hash(source, include_directives)
|
135
|
+
obj_hash['relationships'] = relationships unless relationships.empty?
|
126
136
|
|
127
|
-
|
128
|
-
|
137
|
+
obj_hash['meta'] = source.meta_json if source.meta_json
|
138
|
+
else
|
139
|
+
fetchable_fields = Set.new(source.fetchable_fields)
|
129
140
|
|
130
|
-
|
131
|
-
|
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
|
-
|
134
|
-
|
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
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
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
|
149
|
-
|
150
|
-
|
151
|
-
|
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
|
-
|
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
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
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
|
-
|
270
|
+
if field_set.include?(name)
|
271
|
+
hash[format_key(name)] = link_object(source, relationship, include_linkage)
|
272
|
+
end
|
199
273
|
|
200
|
-
|
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
|
-
|
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
|
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
|
-
|
209
|
-
|
210
|
-
|
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
|
-
|
213
|
-
|
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
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
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
|
-
|
251
|
-
|
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 =
|
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
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
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
|
-
|
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
|
-
|
343
|
-
|
344
|
-
|
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]
|
469
|
+
@included_objects[type] ||= {}
|
470
|
+
existing = @included_objects[type][id]
|
347
471
|
|
348
|
-
if
|
349
|
-
|
350
|
-
|
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
|
-
|
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
|
|