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.
@@ -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
- unless scopes.empty?
61
- "#{ scopes.first.to_s.camelize }::Engine".safe_constantize
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
 
@@ -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
- data = JSON.parse(body)
23
- data = {:_json => data} unless data.is_a?(Hash)
24
- data.with_indifferent_access
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
@@ -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
- resource_klass = relationship.resource_klass
124
- if resource_klass
123
+ if relationship.polymorphic?
125
124
  associated_model = public_send(associated_records_method_name)
126
- return associated_model ? resource_klass.new(associated_model, @context) : nil
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(@resource_klass.module_path + relationship.class_name.to_s.underscore), include_parts.last.partition('.'))
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(include)
213
- return if include.nil?
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 = CSV.parse_line(include)
220
- return if included_resources.nil?
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
- include = []
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
- include.push(unformat_key(included_resource).to_s)
231
+ unformat_key(included_resource).to_s
226
232
  end
227
233
 
228
- @include_directives = JSONAPI::IncludeDirectives.new(@resource_klass, include)
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
- @sort_criteria = CSV.parse_line(URI.unescape(sort_criteria)).collect do |sort|
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
@@ -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
- subclass._relationships = {}
422
- # Add the relationships from the base class to the subclass using the original options
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
- filter_values += raw.is_a?(String) ? CSV.parse_line(raw) : [raw]
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
- return @_model_name = class_name.demodulize.sub(/Resource$/, '')
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 @model if defined?(@model)
978
- return nil if self.name.to_s.blank? && _model_name.to_s.blank?
979
- @model = _model_name.to_s.safe_constantize
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?
981
- @model
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}, relevant_options)
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
- process_primary(source, @include_directives.include_directives)
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
- if object[:primary]
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 process_primary(source, include_directives)
191
+ def process_source_objects(source, include_directives)
172
192
  if source.respond_to?(:to_ary)
173
- source.each { |resource| process_primary(resource, include_directives) }
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{|k,v| source.fetchable_fields.include?(k) }
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].each do |id, f|
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)
@@ -1,5 +1,5 @@
1
1
  module JSONAPI
2
2
  module Resources
3
- VERSION = '0.9.0.beta1'
3
+ VERSION = '0.9.0.beta2'
4
4
  end
5
5
  end
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.beta1
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: 2016-11-11 00:00:00.000000000 Z
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.5.1
220
+ rubygems_version: 2.6.8
221
221
  signing_key:
222
222
  specification_version: 4
223
223
  summary: Easily support JSON API in Rails.