jsonapi-resources 0.9.0.beta1 → 0.9.0.beta2
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 +10 -2126
- data/lib/jsonapi/cached_resource_fragment.rb +28 -20
- data/lib/jsonapi/error_codes.rb +1 -0
- data/lib/jsonapi/exceptions.rb +277 -203
- data/lib/jsonapi/link_builder.rb +6 -2
- data/lib/jsonapi/mime_types.rb +14 -3
- data/lib/jsonapi/paginator.rb +2 -2
- data/lib/jsonapi/relationship_builder.rb +9 -3
- data/lib/jsonapi/request_parser.rb +24 -10
- data/lib/jsonapi/resource.rb +50 -19
- data/lib/jsonapi/resource_serializer.rb +42 -9
- data/lib/jsonapi/resources/version.rb +1 -1
- metadata +3 -3
data/lib/jsonapi/link_builder.rb
CHANGED
|
@@ -57,8 +57,12 @@ module JSONAPI
|
|
|
57
57
|
def build_engine_name
|
|
58
58
|
scopes = module_scopes_from_class(primary_resource_klass)
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
begin
|
|
61
|
+
unless scopes.empty?
|
|
62
|
+
"#{ scopes.first.to_s.camelize }::Engine".safe_constantize
|
|
63
|
+
end
|
|
64
|
+
rescue LoadError => e
|
|
65
|
+
nil
|
|
62
66
|
end
|
|
63
67
|
end
|
|
64
68
|
|
data/lib/jsonapi/mime_types.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
1
3
|
module JSONAPI
|
|
2
4
|
MEDIA_TYPE = 'application/vnd.api+json'
|
|
3
5
|
|
|
@@ -19,9 +21,18 @@ module JSONAPI
|
|
|
19
21
|
|
|
20
22
|
def self.parser
|
|
21
23
|
lambda do |body|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
24
|
+
begin
|
|
25
|
+
data = JSON.parse(body)
|
|
26
|
+
if data.is_a?(Hash)
|
|
27
|
+
data.with_indifferent_access
|
|
28
|
+
else
|
|
29
|
+
fail JSONAPI::Exceptions::InvalidRequestFormat.new
|
|
30
|
+
end
|
|
31
|
+
rescue JSON::ParserError => e
|
|
32
|
+
{ _parser_exception: JSONAPI::Exceptions::BadRequest.new(e.to_s) }
|
|
33
|
+
rescue => e
|
|
34
|
+
{ _parser_exception: e }
|
|
35
|
+
end
|
|
25
36
|
end
|
|
26
37
|
end
|
|
27
38
|
end
|
data/lib/jsonapi/paginator.rb
CHANGED
|
@@ -110,7 +110,7 @@ class OffsetPaginator < JSONAPI::Paginator
|
|
|
110
110
|
fail JSONAPI::Exceptions::InvalidPageValue.new(:limit, @limit)
|
|
111
111
|
elsif @limit > JSONAPI.configuration.maximum_page_size
|
|
112
112
|
fail JSONAPI::Exceptions::InvalidPageValue.new(:limit, @limit,
|
|
113
|
-
"Limit exceeds maximum page size of #{JSONAPI.configuration.maximum_page_size}.")
|
|
113
|
+
detail: "Limit exceeds maximum page size of #{JSONAPI.configuration.maximum_page_size}.")
|
|
114
114
|
end
|
|
115
115
|
|
|
116
116
|
if @offset < 0
|
|
@@ -199,7 +199,7 @@ class PagedPaginator < JSONAPI::Paginator
|
|
|
199
199
|
fail JSONAPI::Exceptions::InvalidPageValue.new(:size, @size)
|
|
200
200
|
elsif @size > JSONAPI.configuration.maximum_page_size
|
|
201
201
|
fail JSONAPI::Exceptions::InvalidPageValue.new(:size, @size,
|
|
202
|
-
"size exceeds maximum page size of #{JSONAPI.configuration.maximum_page_size}.")
|
|
202
|
+
detail: "size exceeds maximum page size of #{JSONAPI.configuration.maximum_page_size}.")
|
|
203
203
|
end
|
|
204
204
|
|
|
205
205
|
if @number < 1
|
|
@@ -120,10 +120,16 @@ module JSONAPI
|
|
|
120
120
|
define_on_resource relationship_name do |options = {}|
|
|
121
121
|
relationship = self.class._relationships[relationship_name]
|
|
122
122
|
|
|
123
|
-
|
|
124
|
-
if resource_klass
|
|
123
|
+
if relationship.polymorphic?
|
|
125
124
|
associated_model = public_send(associated_records_method_name)
|
|
126
|
-
|
|
125
|
+
resource_klass = self.class.resource_for_model(associated_model) if associated_model
|
|
126
|
+
return resource_klass.new(associated_model, @context) if resource_klass && associated_model
|
|
127
|
+
else
|
|
128
|
+
resource_klass = relationship.resource_klass
|
|
129
|
+
if resource_klass
|
|
130
|
+
associated_model = public_send(associated_records_method_name)
|
|
131
|
+
return associated_model ? resource_klass.new(associated_model, @context) : nil
|
|
132
|
+
end
|
|
127
133
|
end
|
|
128
134
|
end
|
|
129
135
|
end
|
|
@@ -34,6 +34,7 @@ module JSONAPI
|
|
|
34
34
|
|
|
35
35
|
setup_action_method_name = "setup_#{params[:action]}_action"
|
|
36
36
|
if respond_to?(setup_action_method_name)
|
|
37
|
+
raise params[:_parser_exception] if params[:_parser_exception]
|
|
37
38
|
send(setup_action_method_name, params)
|
|
38
39
|
end
|
|
39
40
|
rescue ActionController::ParameterMissing => e
|
|
@@ -201,7 +202,7 @@ module JSONAPI
|
|
|
201
202
|
relationship = resource_klass._relationship(relationship_name)
|
|
202
203
|
if relationship && format_key(relationship_name) == include_parts.first
|
|
203
204
|
unless include_parts.last.empty?
|
|
204
|
-
check_include(Resource.resource_for(
|
|
205
|
+
check_include(Resource.resource_for(resource_klass.module_path + relationship.class_name.to_s.underscore), include_parts.last.partition('.'))
|
|
205
206
|
end
|
|
206
207
|
else
|
|
207
208
|
@errors.concat(JSONAPI::Exceptions::InvalidInclude.new(format_key(resource_klass._type),
|
|
@@ -209,23 +210,28 @@ module JSONAPI
|
|
|
209
210
|
end
|
|
210
211
|
end
|
|
211
212
|
|
|
212
|
-
def parse_include_directives(
|
|
213
|
-
return
|
|
213
|
+
def parse_include_directives(raw_include)
|
|
214
|
+
return unless raw_include
|
|
214
215
|
|
|
215
216
|
unless JSONAPI.configuration.allow_include
|
|
216
217
|
fail JSONAPI::Exceptions::ParametersNotAllowed.new([:include])
|
|
217
218
|
end
|
|
218
219
|
|
|
219
|
-
included_resources =
|
|
220
|
-
|
|
220
|
+
included_resources = []
|
|
221
|
+
begin
|
|
222
|
+
included_resources += CSV.parse_line(raw_include)
|
|
223
|
+
rescue CSV::MalformedCSVError
|
|
224
|
+
fail JSONAPI::Exceptions::InvalidInclude.new(format_key(@resource_klass._type), raw_include)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
return if included_resources.empty?
|
|
221
228
|
|
|
222
|
-
|
|
223
|
-
included_resources.each do |included_resource|
|
|
229
|
+
result = included_resources.map do |included_resource|
|
|
224
230
|
check_include(@resource_klass, included_resource.partition('.'))
|
|
225
|
-
|
|
231
|
+
unformat_key(included_resource).to_s
|
|
226
232
|
end
|
|
227
233
|
|
|
228
|
-
@include_directives = JSONAPI::IncludeDirectives.new(@resource_klass,
|
|
234
|
+
@include_directives = JSONAPI::IncludeDirectives.new(@resource_klass, result)
|
|
229
235
|
end
|
|
230
236
|
|
|
231
237
|
def parse_filters(filters)
|
|
@@ -264,7 +270,15 @@ module JSONAPI
|
|
|
264
270
|
fail JSONAPI::Exceptions::ParametersNotAllowed.new([:sort])
|
|
265
271
|
end
|
|
266
272
|
|
|
267
|
-
|
|
273
|
+
sorts = []
|
|
274
|
+
begin
|
|
275
|
+
raw = URI.unescape(sort_criteria)
|
|
276
|
+
sorts += CSV.parse_line(raw)
|
|
277
|
+
rescue CSV::MalformedCSVError
|
|
278
|
+
fail JSONAPI::Exceptions::InvalidSortCriteria.new(format_key(@resource_klass._type), raw)
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
@sort_criteria = sorts.collect do |sort|
|
|
268
282
|
if sort.start_with?('-')
|
|
269
283
|
sort_criteria = { field: unformat_key(sort[1..-1]).to_s }
|
|
270
284
|
sort_criteria[:direction] = :desc
|
data/lib/jsonapi/resource.rb
CHANGED
|
@@ -416,18 +416,15 @@ module JSONAPI
|
|
|
416
416
|
subclass.immutable(false)
|
|
417
417
|
subclass.caching(false)
|
|
418
418
|
subclass._attributes = (_attributes || {}).dup
|
|
419
|
+
|
|
419
420
|
subclass._model_hints = (_model_hints || {}).dup
|
|
420
421
|
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
if _relationships.is_a?(Hash)
|
|
424
|
-
_relationships.each_value do |relationship|
|
|
425
|
-
options = relationship.options.dup
|
|
426
|
-
options[:parent_resource] = subclass
|
|
427
|
-
subclass._add_relationship(relationship.class, relationship.name, options)
|
|
428
|
-
end
|
|
422
|
+
unless _model_name.empty?
|
|
423
|
+
subclass.model_name(_model_name, add_model_hint: (_model_hints && !_model_hints[_model_name].nil?) == true)
|
|
429
424
|
end
|
|
430
425
|
|
|
426
|
+
subclass.rebuild_relationships(_relationships || {})
|
|
427
|
+
|
|
431
428
|
subclass._allowed_filters = (_allowed_filters || Set.new).dup
|
|
432
429
|
|
|
433
430
|
type = subclass.name.demodulize.sub(/Resource$/, '').underscore
|
|
@@ -438,6 +435,20 @@ module JSONAPI
|
|
|
438
435
|
check_reserved_resource_name(subclass._type, subclass.name)
|
|
439
436
|
end
|
|
440
437
|
|
|
438
|
+
def rebuild_relationships(relationships)
|
|
439
|
+
original_relationships = relationships.deep_dup
|
|
440
|
+
|
|
441
|
+
@_relationships = {}
|
|
442
|
+
|
|
443
|
+
if original_relationships.is_a?(Hash)
|
|
444
|
+
original_relationships.each_value do |relationship|
|
|
445
|
+
options = relationship.options.dup
|
|
446
|
+
options[:parent_resource] = self
|
|
447
|
+
_add_relationship(relationship.class, relationship.name, options)
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
441
452
|
def resource_for(type)
|
|
442
453
|
type = type.underscore
|
|
443
454
|
type_with_module = type.include?('/') ? type : module_path + type
|
|
@@ -554,6 +565,8 @@ module JSONAPI
|
|
|
554
565
|
@_model_name = model.to_sym
|
|
555
566
|
|
|
556
567
|
model_hint(model: @_model_name, resource: self) unless options[:add_model_hint] == false
|
|
568
|
+
|
|
569
|
+
rebuild_relationships(_relationships)
|
|
557
570
|
end
|
|
558
571
|
|
|
559
572
|
def model_hint(model: _model_name, resource: _type)
|
|
@@ -807,7 +820,11 @@ module JSONAPI
|
|
|
807
820
|
def verify_filter(filter, raw, context = nil)
|
|
808
821
|
filter_values = []
|
|
809
822
|
if raw.present?
|
|
810
|
-
|
|
823
|
+
begin
|
|
824
|
+
filter_values += raw.is_a?(String) ? CSV.parse_line(raw) : [raw]
|
|
825
|
+
rescue CSV::MalformedCSVError
|
|
826
|
+
filter_values << raw
|
|
827
|
+
end
|
|
811
828
|
end
|
|
812
829
|
|
|
813
830
|
strategy = _allowed_filters.fetch(filter, Hash.new)[:verify]
|
|
@@ -900,10 +917,11 @@ module JSONAPI
|
|
|
900
917
|
if _abstract
|
|
901
918
|
return ''
|
|
902
919
|
else
|
|
903
|
-
return @_model_name if defined?(@_model_name)
|
|
920
|
+
return @_model_name.to_s if defined?(@_model_name)
|
|
904
921
|
class_name = self.name
|
|
905
922
|
return '' if class_name.nil?
|
|
906
|
-
|
|
923
|
+
@_model_name = class_name.demodulize.sub(/Resource$/, '')
|
|
924
|
+
return @_model_name.to_s
|
|
907
925
|
end
|
|
908
926
|
end
|
|
909
927
|
|
|
@@ -974,11 +992,17 @@ module JSONAPI
|
|
|
974
992
|
def _model_class
|
|
975
993
|
return nil if _abstract
|
|
976
994
|
|
|
977
|
-
return @
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
995
|
+
return @model_class if @model_class
|
|
996
|
+
|
|
997
|
+
model_name = _model_name
|
|
998
|
+
return nil if model_name.to_s.blank?
|
|
999
|
+
|
|
1000
|
+
@model_class = model_name.to_s.safe_constantize
|
|
1001
|
+
if @model_class.nil?
|
|
1002
|
+
warn "[MODEL NOT FOUND] Model could not be found for #{self.name}. If this a base Resource declare it as abstract."
|
|
1003
|
+
end
|
|
1004
|
+
|
|
1005
|
+
@model_class
|
|
982
1006
|
end
|
|
983
1007
|
|
|
984
1008
|
def _allowed_filter?(filter)
|
|
@@ -1040,7 +1064,7 @@ module JSONAPI
|
|
|
1040
1064
|
cache_ids = pluck_arel_attributes(records, t[_primary_key], t[_cache_field])
|
|
1041
1065
|
resources = CachedResourceFragment.fetch_fragments(self, serializer, options[:context], cache_ids)
|
|
1042
1066
|
else
|
|
1043
|
-
resources = resources_for(records, options).map{|r| [r.id, r] }.to_h
|
|
1067
|
+
resources = resources_for(records, options[:context]).map{|r| [r.id, r] }.to_h
|
|
1044
1068
|
end
|
|
1045
1069
|
|
|
1046
1070
|
preload_included_fragments(resources, records, serializer, options)
|
|
@@ -1102,7 +1126,6 @@ module JSONAPI
|
|
|
1102
1126
|
include_directives = options[:include_directives]
|
|
1103
1127
|
return unless include_directives
|
|
1104
1128
|
|
|
1105
|
-
relevant_options = options.except(:include_directives, :order, :paginator)
|
|
1106
1129
|
context = options[:context]
|
|
1107
1130
|
|
|
1108
1131
|
# For each association, including indirect associations, find the target record ids.
|
|
@@ -1133,8 +1156,15 @@ module JSONAPI
|
|
|
1133
1156
|
|
|
1134
1157
|
# For each step on the path, figure out what the actual table name/alias in the join
|
|
1135
1158
|
# will be, and include the primary key of that table in our list of fields to select
|
|
1159
|
+
non_polymorphic = true
|
|
1136
1160
|
path.each do |elem|
|
|
1137
1161
|
relationship = klass._relationships[elem]
|
|
1162
|
+
if relationship.polymorphic
|
|
1163
|
+
# Can't preload through a polymorphic belongs_to association, ResourceSerializer
|
|
1164
|
+
# will just have to bypass the cache and load the real Resource.
|
|
1165
|
+
non_polymorphic = false
|
|
1166
|
+
break
|
|
1167
|
+
end
|
|
1138
1168
|
assocs_path << relationship.relation_name(options).to_sym
|
|
1139
1169
|
# Converts [:a, :b, :c] to Rails-style { :a => { :b => :c }}
|
|
1140
1170
|
ar_hash = assocs_path.reverse.reduce{|memo, step| { step => memo } }
|
|
@@ -1148,6 +1178,7 @@ module JSONAPI
|
|
|
1148
1178
|
klass = relationship.resource_klass
|
|
1149
1179
|
pluck_attrs << table[klass._primary_key]
|
|
1150
1180
|
end
|
|
1181
|
+
next unless non_polymorphic
|
|
1151
1182
|
|
|
1152
1183
|
# Pre-fill empty hashes for each resource up to the end of the path.
|
|
1153
1184
|
# This allows us to later distinguish between a preload that returned nothing
|
|
@@ -1188,7 +1219,7 @@ module JSONAPI
|
|
|
1188
1219
|
.map(&:last)
|
|
1189
1220
|
.reject{|id| target_resources[klass.name].has_key?(id) }
|
|
1190
1221
|
.uniq
|
|
1191
|
-
found = klass.find({klass._primary_key => sub_res_ids},
|
|
1222
|
+
found = klass.find({klass._primary_key => sub_res_ids}, context: options[:context])
|
|
1192
1223
|
target_resources[klass.name].merge! found.map{|r| [r.id, r] }.to_h
|
|
1193
1224
|
end
|
|
1194
1225
|
|
|
@@ -50,15 +50,35 @@ module JSONAPI
|
|
|
50
50
|
|
|
51
51
|
@included_objects = {}
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
process_source_objects(source, @include_directives.include_directives)
|
|
54
54
|
|
|
55
|
-
included_objects = []
|
|
56
55
|
primary_objects = []
|
|
56
|
+
|
|
57
|
+
# pull the processed objects corresponding to the source objects. Ensures we preserve order.
|
|
58
|
+
if is_resource_collection
|
|
59
|
+
source.each do |primary|
|
|
60
|
+
if primary.id
|
|
61
|
+
case primary
|
|
62
|
+
when CachedResourceFragment then primary_objects.push(@included_objects[primary.type][primary.id][:object_hash])
|
|
63
|
+
when Resource then primary_objects.push(@included_objects[primary.class._type][primary.id][:object_hash])
|
|
64
|
+
else raise "Unknown source type #{primary.inspect}"
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
else
|
|
69
|
+
if source.try(:id)
|
|
70
|
+
case source
|
|
71
|
+
when CachedResourceFragment then primary_objects.push(@included_objects[source.type][source.id][:object_hash])
|
|
72
|
+
when Resource then primary_objects.push(@included_objects[source.class._type][source.id][:object_hash])
|
|
73
|
+
else raise "Unknown source type #{source.inspect}"
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
included_objects = []
|
|
57
79
|
@included_objects.each_value do |objects|
|
|
58
80
|
objects.each_value do |object|
|
|
59
|
-
|
|
60
|
-
primary_objects.push(object[:object_hash])
|
|
61
|
-
else
|
|
81
|
+
unless object[:primary]
|
|
62
82
|
included_objects.push(object[:object_hash])
|
|
63
83
|
end
|
|
64
84
|
end
|
|
@@ -168,9 +188,9 @@ module JSONAPI
|
|
|
168
188
|
# requested includes. Fields are controlled fields option for each resource type, such
|
|
169
189
|
# as fields: { people: [:id, :email, :comments], posts: [:id, :title, :author], comments: [:id, :body, :post]}
|
|
170
190
|
# The fields options controls both fields and included links references.
|
|
171
|
-
def
|
|
191
|
+
def process_source_objects(source, include_directives)
|
|
172
192
|
if source.respond_to?(:to_ary)
|
|
173
|
-
source.each { |resource|
|
|
193
|
+
source.each { |resource| process_source_objects(resource, include_directives) }
|
|
174
194
|
else
|
|
175
195
|
return {} if source.nil?
|
|
176
196
|
add_resource(source, include_directives, true)
|
|
@@ -298,8 +318,11 @@ module JSONAPI
|
|
|
298
318
|
h = source.relationships || {}
|
|
299
319
|
return h unless include_directives.has_key?(:include_related)
|
|
300
320
|
|
|
301
|
-
relationships = source.resource_klass._relationships.select
|
|
321
|
+
relationships = source.resource_klass._relationships.select do |k,v|
|
|
322
|
+
source.fetchable_fields.include?(k)
|
|
323
|
+
end
|
|
302
324
|
|
|
325
|
+
real_res = nil
|
|
303
326
|
relationships.each do |rel_name, relationship|
|
|
304
327
|
key = @key_formatter.format(rel_name)
|
|
305
328
|
to_many = relationship.is_a? JSONAPI::Relationship::ToMany
|
|
@@ -310,7 +333,17 @@ module JSONAPI
|
|
|
310
333
|
h[key][:data] = to_many ? [] : nil
|
|
311
334
|
end
|
|
312
335
|
|
|
313
|
-
source.preloaded_fragments[key]
|
|
336
|
+
fragments = source.preloaded_fragments[key]
|
|
337
|
+
if fragments.nil?
|
|
338
|
+
# The resources we want were not preloaded, we'll have to bypass the cache.
|
|
339
|
+
# This happens when including through belongs_to polymorphic relationships
|
|
340
|
+
if real_res.nil?
|
|
341
|
+
real_res = source.to_real_resource
|
|
342
|
+
end
|
|
343
|
+
relation_resources = [real_res.public_send(rel_name)].flatten(1).compact
|
|
344
|
+
fragments = relation_resources.map{|r| [r.id, r]}.to_h
|
|
345
|
+
end
|
|
346
|
+
fragments.each do |id, f|
|
|
314
347
|
add_resource(f, ia)
|
|
315
348
|
|
|
316
349
|
if h.has_key?(key)
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: jsonapi-resources
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.9.0.
|
|
4
|
+
version: 0.9.0.beta2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Dan Gebhardt
|
|
@@ -9,7 +9,7 @@ authors:
|
|
|
9
9
|
autorequire:
|
|
10
10
|
bindir: bin
|
|
11
11
|
cert_chain: []
|
|
12
|
-
date:
|
|
12
|
+
date: 2017-01-17 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: bundler
|
|
@@ -217,7 +217,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
217
217
|
version: 1.3.1
|
|
218
218
|
requirements: []
|
|
219
219
|
rubyforge_project:
|
|
220
|
-
rubygems_version: 2.
|
|
220
|
+
rubygems_version: 2.6.8
|
|
221
221
|
signing_key:
|
|
222
222
|
specification_version: 4
|
|
223
223
|
summary: Easily support JSON API in Rails.
|