active_model_serializers 0.10.5 → 0.10.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/CHANGELOG.md +16 -2
  4. data/README.md +2 -2
  5. data/Rakefile +1 -30
  6. data/active_model_serializers.gemspec +1 -1
  7. data/bin/rubocop +38 -0
  8. data/docs/general/adapters.md +25 -9
  9. data/docs/general/getting_started.md +1 -1
  10. data/docs/general/rendering.md +18 -4
  11. data/docs/general/serializers.md +21 -2
  12. data/docs/howto/add_pagination_links.md +1 -1
  13. data/docs/integrations/ember-and-json-api.md +3 -0
  14. data/lib/active_model/serializer.rb +252 -74
  15. data/lib/active_model/serializer/association.rb +51 -14
  16. data/lib/active_model/serializer/belongs_to_reflection.rb +5 -1
  17. data/lib/active_model/serializer/concerns/caching.rb +29 -21
  18. data/lib/active_model/serializer/has_many_reflection.rb +4 -1
  19. data/lib/active_model/serializer/has_one_reflection.rb +1 -1
  20. data/lib/active_model/serializer/lazy_association.rb +95 -0
  21. data/lib/active_model/serializer/reflection.rb +119 -75
  22. data/lib/active_model/serializer/version.rb +1 -1
  23. data/lib/active_model_serializers/adapter/json_api.rb +30 -17
  24. data/lib/active_model_serializers/adapter/json_api/relationship.rb +38 -9
  25. data/lib/active_model_serializers/adapter/json_api/resource_identifier.rb +9 -0
  26. data/lib/active_model_serializers/model.rb +5 -4
  27. data/lib/tasks/rubocop.rake +53 -0
  28. data/test/action_controller/adapter_selector_test.rb +2 -2
  29. data/test/serializers/associations_test.rb +64 -31
  30. data/test/serializers/reflection_test.rb +427 -0
  31. metadata +9 -12
  32. data/lib/active_model/serializer/collection_reflection.rb +0 -7
  33. data/lib/active_model/serializer/concerns/associations.rb +0 -102
  34. data/lib/active_model/serializer/concerns/attributes.rb +0 -82
  35. data/lib/active_model/serializer/concerns/configuration.rb +0 -59
  36. data/lib/active_model/serializer/concerns/links.rb +0 -35
  37. data/lib/active_model/serializer/concerns/meta.rb +0 -29
  38. data/lib/active_model/serializer/concerns/type.rb +0 -25
  39. data/lib/active_model/serializer/singular_reflection.rb +0 -7
@@ -1,5 +1,5 @@
1
1
  module ActiveModel
2
2
  class Serializer
3
- VERSION = '0.10.5'.freeze
3
+ VERSION = '0.10.6'.freeze
4
4
  end
5
5
  end
@@ -257,7 +257,8 @@ module ActiveModelSerializers
257
257
 
258
258
  def process_relationships(serializer, include_slice)
259
259
  serializer.associations(include_slice).each do |association|
260
- process_relationship(association.serializer, include_slice[association.key])
260
+ # TODO(BF): Process relationship without evaluating lazy_association
261
+ process_relationship(association.lazy_association.serializer, include_slice[association.key])
261
262
  end
262
263
  end
263
264
 
@@ -294,20 +295,8 @@ module ActiveModelSerializers
294
295
 
295
296
  # {http://jsonapi.org/format/#document-resource-objects Document Resource Objects}
296
297
  def resource_object_for(serializer, include_slice = {})
297
- resource_object = serializer.fetch(self) do
298
- resource_object = ResourceIdentifier.new(serializer, instance_options).as_json
299
-
300
- requested_fields = fieldset && fieldset.fields_for(resource_object[:type])
301
- attributes = attributes_for(serializer, requested_fields)
302
- resource_object[:attributes] = attributes if attributes.any?
303
- resource_object
304
- end
305
-
306
- requested_associations = fieldset.fields_for(resource_object[:type]) || '*'
307
- relationships = relationships_for(serializer, requested_associations, include_slice)
308
- resource_object[:relationships] = relationships if relationships.any?
298
+ resource_object = data_for(serializer, include_slice)
309
299
 
310
- links = links_for(serializer)
311
300
  # toplevel_links
312
301
  # definition:
313
302
  # allOf
@@ -321,7 +310,10 @@ module ActiveModelSerializers
321
310
  # prs:
322
311
  # https://github.com/rails-api/active_model_serializers/pull/1247
323
312
  # https://github.com/rails-api/active_model_serializers/pull/1018
324
- resource_object[:links] = links if links.any?
313
+ if (links = links_for(serializer)).any?
314
+ resource_object ||= {}
315
+ resource_object[:links] = links
316
+ end
325
317
 
326
318
  # toplevel_meta
327
319
  # alias meta
@@ -331,12 +323,33 @@ module ActiveModelSerializers
331
323
  # {
332
324
  # :'git-ref' => 'abc123'
333
325
  # }
334
- meta = meta_for(serializer)
335
- resource_object[:meta] = meta unless meta.blank?
326
+ if (meta = meta_for(serializer)).present?
327
+ resource_object ||= {}
328
+ resource_object[:meta] = meta
329
+ end
336
330
 
337
331
  resource_object
338
332
  end
339
333
 
334
+ def data_for(serializer, include_slice)
335
+ data = serializer.fetch(self) do
336
+ resource_object = ResourceIdentifier.new(serializer, instance_options).as_json
337
+ break nil if resource_object.nil?
338
+
339
+ requested_fields = fieldset && fieldset.fields_for(resource_object[:type])
340
+ attributes = attributes_for(serializer, requested_fields)
341
+ resource_object[:attributes] = attributes if attributes.any?
342
+ resource_object
343
+ end
344
+ data.tap do |resource_object|
345
+ next if resource_object.nil?
346
+ # NOTE(BF): the attributes are cached above, separately from the relationships, below.
347
+ requested_associations = fieldset.fields_for(resource_object[:type]) || '*'
348
+ relationships = relationships_for(serializer, requested_associations, include_slice)
349
+ resource_object[:relationships] = relationships if relationships.any?
350
+ end
351
+ end
352
+
340
353
  # {http://jsonapi.org/format/#document-resource-object-relationships Document Resource Object Relationship}
341
354
  # relationships
342
355
  # definition:
@@ -15,9 +15,7 @@ module ActiveModelSerializers
15
15
  def as_json
16
16
  hash = {}
17
17
 
18
- if association.options[:include_data]
19
- hash[:data] = data_for(association)
20
- end
18
+ hash[:data] = data_for(association) if association.include_data?
21
19
 
22
20
  links = links_for(association)
23
21
  hash[:links] = links if links.any?
@@ -35,14 +33,45 @@ module ActiveModelSerializers
35
33
 
36
34
  private
37
35
 
36
+ # TODO(BF): Avoid db hit on belong_to_ releationship by using foreign_key on self
38
37
  def data_for(association)
39
- serializer = association.serializer
40
- if serializer.respond_to?(:each)
41
- serializer.map { |s| ResourceIdentifier.new(s, serializable_resource_options).as_json }
42
- elsif (virtual_value = association.options[:virtual_value])
38
+ if association.collection?
39
+ data_for_many(association)
40
+ else
41
+ data_for_one(association)
42
+ end
43
+ end
44
+
45
+ def data_for_one(association)
46
+ if association.belongs_to? &&
47
+ parent_serializer.object.respond_to?(association.reflection.foreign_key)
48
+ id = parent_serializer.object.send(association.reflection.foreign_key)
49
+ type = association.reflection.type.to_s
50
+ ResourceIdentifier.for_type_with_id(type, id, serializable_resource_options)
51
+ else
52
+ # TODO(BF): Process relationship without evaluating lazy_association
53
+ serializer = association.lazy_association.serializer
54
+ if (virtual_value = association.virtual_value)
55
+ virtual_value
56
+ elsif serializer && association.object
57
+ ResourceIdentifier.new(serializer, serializable_resource_options).as_json
58
+ else
59
+ nil
60
+ end
61
+ end
62
+ end
63
+
64
+ def data_for_many(association)
65
+ # TODO(BF): Process relationship without evaluating lazy_association
66
+ collection_serializer = association.lazy_association.serializer
67
+ if collection_serializer.respond_to?(:each)
68
+ collection_serializer.map do |serializer|
69
+ ResourceIdentifier.new(serializer, serializable_resource_options).as_json
70
+ end
71
+ elsif (virtual_value = association.virtual_value)
43
72
  virtual_value
44
- elsif serializer && serializer.object
45
- ResourceIdentifier.new(serializer, serializable_resource_options).as_json
73
+ else
74
+ []
46
75
  end
47
76
  end
48
77
 
@@ -22,6 +22,14 @@ module ActiveModelSerializers
22
22
  JsonApi.send(:transform_key_casing!, raw_type, transform_options)
23
23
  end
24
24
 
25
+ def self.for_type_with_id(type, id, options)
26
+ return nil if id.blank?
27
+ {
28
+ id: id.to_s,
29
+ type: type_for(:no_class_needed, type, options)
30
+ }
31
+ end
32
+
25
33
  # {http://jsonapi.org/format/#document-resource-identifier-objects Resource Identifier Objects}
26
34
  def initialize(serializer, options)
27
35
  @id = id_for(serializer)
@@ -29,6 +37,7 @@ module ActiveModelSerializers
29
37
  end
30
38
 
31
39
  def as_json
40
+ return nil if id.blank?
32
41
  { id: id, type: type }
33
42
  end
34
43
 
@@ -1,12 +1,13 @@
1
1
  # ActiveModelSerializers::Model is a convenient superclass for making your models
2
2
  # from Plain-Old Ruby Objects (PORO). It also serves as a reference implementation
3
3
  # that satisfies ActiveModel::Serializer::Lint::Tests.
4
+ require 'active_support/core_ext/hash'
4
5
  module ActiveModelSerializers
5
6
  class Model
6
7
  include ActiveModel::Serializers::JSON
7
8
  include ActiveModel::Model
8
9
 
9
- # Declare names of attributes to be included in +sttributes+ hash.
10
+ # Declare names of attributes to be included in +attributes+ hash.
10
11
  # Is only available as a class-method since the ActiveModel::Serialization mixin in Rails
11
12
  # uses an +attribute_names+ local variable, which may conflict if we were to add instance methods here.
12
13
  #
@@ -19,8 +20,8 @@ module ActiveModelSerializers
19
20
 
20
21
  # Easily declare instance attributes with setters and getters for each.
21
22
  #
22
- # All attributes to initialize an instance must have setters.
23
- # However, the hash turned by +attributes+ instance method will ALWAYS
23
+ # To initialize an instance, all attributes must have setters.
24
+ # However, the hash returned by +attributes+ instance method will ALWAYS
24
25
  # be the value of the initial attributes, regardless of what accessors are defined.
25
26
  # The only way to change the change the attributes after initialization is
26
27
  # to mutate the +attributes+ directly.
@@ -58,7 +59,7 @@ module ActiveModelSerializers
58
59
 
59
60
  # Override the +attributes+ method so that the hash is derived from +attribute_names+.
60
61
  #
61
- # The the fields in +attribute_names+ determines the returned hash.
62
+ # The fields in +attribute_names+ determines the returned hash.
62
63
  # +attributes+ are returned frozen to prevent any expectations that mutation affects
63
64
  # the actual values in the model.
64
65
  def attributes
@@ -0,0 +1,53 @@
1
+ begin
2
+ require 'rubocop'
3
+ require 'rubocop/rake_task'
4
+ rescue LoadError # rubocop:disable Lint/HandleExceptions
5
+ else
6
+ require 'rbconfig'
7
+ # https://github.com/bundler/bundler/blob/1b3eb2465a/lib/bundler/constants.rb#L2
8
+ windows_platforms = /(msdos|mswin|djgpp|mingw)/
9
+ if RbConfig::CONFIG['host_os'] =~ windows_platforms
10
+ desc 'No-op rubocop on Windows-- unsupported platform'
11
+ task :rubocop do
12
+ puts 'Skipping rubocop on Windows'
13
+ end
14
+ elsif defined?(::Rubinius)
15
+ desc 'No-op rubocop to avoid rbx segfault'
16
+ task :rubocop do
17
+ puts 'Skipping rubocop on rbx due to segfault'
18
+ puts 'https://github.com/rubinius/rubinius/issues/3499'
19
+ end
20
+ else
21
+ Rake::Task[:rubocop].clear if Rake::Task.task_defined?(:rubocop)
22
+ patterns = [
23
+ 'Gemfile',
24
+ 'Rakefile',
25
+ 'lib/**/*.{rb,rake}',
26
+ 'config/**/*.rb',
27
+ 'app/**/*.rb',
28
+ 'test/**/*.rb'
29
+ ]
30
+ desc 'Execute rubocop'
31
+ RuboCop::RakeTask.new(:rubocop) do |task|
32
+ task.options = ['--rails', '--display-cop-names', '--display-style-guide']
33
+ task.formatters = ['progress']
34
+ task.patterns = patterns
35
+ task.fail_on_error = true
36
+ end
37
+
38
+ namespace :rubocop do
39
+ desc 'Auto-gen rubocop config'
40
+ task :auto_gen_config do
41
+ options = ['--auto-gen-config'].concat patterns
42
+ require 'benchmark'
43
+ result = 0
44
+ cli = RuboCop::CLI.new
45
+ time = Benchmark.realtime do
46
+ result = cli.run(options)
47
+ end
48
+ puts "Finished in #{time} seconds" if cli.options[:debug]
49
+ abort('RuboCop failed!') if result.nonzero?
50
+ end
51
+ end
52
+ end
53
+ end
@@ -19,7 +19,7 @@ module ActionController
19
19
  end
20
20
 
21
21
  def render_using_adapter_override
22
- @profile = Profile.new(name: 'Name 1', description: 'Description 1', comments: 'Comments 1')
22
+ @profile = Profile.new(id: 'render_using_adapter_override', name: 'Name 1', description: 'Description 1', comments: 'Comments 1')
23
23
  render json: @profile, adapter: :json_api
24
24
  end
25
25
 
@@ -41,7 +41,7 @@ module ActionController
41
41
 
42
42
  expected = {
43
43
  data: {
44
- id: @controller.instance_variable_get(:@profile).id.to_s,
44
+ id: 'render_using_adapter_override',
45
45
  type: 'profiles',
46
46
  attributes: {
47
47
  name: 'Name 1',
@@ -30,18 +30,17 @@ module ActiveModel
30
30
  def test_has_many_and_has_one
31
31
  @author_serializer.associations.each do |association|
32
32
  key = association.key
33
- serializer = association.serializer
34
- options = association.options
33
+ serializer = association.lazy_association.serializer
35
34
 
36
35
  case key
37
36
  when :posts
38
- assert_equal true, options.fetch(:include_data)
37
+ assert_equal true, association.include_data?
39
38
  assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer)
40
39
  when :bio
41
- assert_equal true, options.fetch(:include_data)
40
+ assert_equal true, association.include_data?
42
41
  assert_nil serializer
43
42
  when :roles
44
- assert_equal true, options.fetch(:include_data)
43
+ assert_equal true, association.include_data?
45
44
  assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer)
46
45
  else
47
46
  flunk "Unknown association: #{key}"
@@ -56,12 +55,11 @@ module ActiveModel
56
55
  end
57
56
  post_serializer_class.new(@post).associations.each do |association|
58
57
  key = association.key
59
- serializer = association.serializer
60
- options = association.options
58
+ serializer = association.lazy_association.serializer
61
59
 
62
60
  assert_equal :tags, key
63
61
  assert_nil serializer
64
- assert_equal [{ id: 'tagid', name: '#hashtagged' }].to_json, options[:virtual_value].to_json
62
+ assert_equal [{ id: 'tagid', name: '#hashtagged' }].to_json, association.virtual_value.to_json
65
63
  end
66
64
  end
67
65
 
@@ -70,7 +68,7 @@ module ActiveModel
70
68
  .associations
71
69
  .detect { |assoc| assoc.key == :comments }
72
70
 
73
- comment_serializer = association.serializer.first
71
+ comment_serializer = association.lazy_association.serializer.first
74
72
  class << comment_serializer
75
73
  def custom_options
76
74
  instance_options
@@ -82,7 +80,7 @@ module ActiveModel
82
80
  def test_belongs_to
83
81
  @comment_serializer.associations.each do |association|
84
82
  key = association.key
85
- serializer = association.serializer
83
+ serializer = association.lazy_association.serializer
86
84
 
87
85
  case key
88
86
  when :post
@@ -93,7 +91,7 @@ module ActiveModel
93
91
  flunk "Unknown association: #{key}"
94
92
  end
95
93
 
96
- assert_equal true, association.options.fetch(:include_data)
94
+ assert_equal true, association.include_data?
97
95
  end
98
96
  end
99
97
 
@@ -139,6 +137,34 @@ module ActiveModel
139
137
  assert expected_association_keys.include? :site
140
138
  end
141
139
 
140
+ class BelongsToBlogModel < ::Model
141
+ attributes :id, :title
142
+ associations :blog
143
+ end
144
+ class BelongsToBlogModelSerializer < ActiveModel::Serializer
145
+ type :posts
146
+ belongs_to :blog
147
+ end
148
+
149
+ def test_belongs_to_doesnt_load_record
150
+ attributes = { id: 1, title: 'Belongs to Blog', blog: Blog.new(id: 5) }
151
+ post = BelongsToBlogModel.new(attributes)
152
+ class << post
153
+ def blog
154
+ fail 'should use blog_id'
155
+ end
156
+
157
+ def blog_id
158
+ 5
159
+ end
160
+ end
161
+
162
+ actual = serializable(post, adapter: :json_api, serializer: BelongsToBlogModelSerializer).as_json
163
+ expected = { data: { id: '1', type: 'posts', relationships: { blog: { data: { id: '5', type: 'blogs' } } } } }
164
+
165
+ assert_equal expected, actual
166
+ end
167
+
142
168
  class InlineAssociationTestPostSerializer < ActiveModel::Serializer
143
169
  has_many :comments
144
170
  has_many :comments, key: :last_comments do
@@ -203,11 +229,11 @@ module ActiveModel
203
229
  @post_serializer.associations.each do |association|
204
230
  case association.key
205
231
  when :comments
206
- assert_instance_of(ResourceNamespace::CommentSerializer, association.serializer.first)
232
+ assert_instance_of(ResourceNamespace::CommentSerializer, association.lazy_association.serializer.first)
207
233
  when :author
208
- assert_instance_of(ResourceNamespace::AuthorSerializer, association.serializer)
234
+ assert_instance_of(ResourceNamespace::AuthorSerializer, association.lazy_association.serializer)
209
235
  when :description
210
- assert_instance_of(ResourceNamespace::DescriptionSerializer, association.serializer)
236
+ assert_instance_of(ResourceNamespace::DescriptionSerializer, association.lazy_association.serializer)
211
237
  else
212
238
  flunk "Unknown association: #{key}"
213
239
  end
@@ -245,11 +271,11 @@ module ActiveModel
245
271
  @post_serializer.associations.each do |association|
246
272
  case association.key
247
273
  when :comments
248
- assert_instance_of(PostSerializer::CommentSerializer, association.serializer.first)
274
+ assert_instance_of(PostSerializer::CommentSerializer, association.lazy_association.serializer.first)
249
275
  when :author
250
- assert_instance_of(PostSerializer::AuthorSerializer, association.serializer)
276
+ assert_instance_of(PostSerializer::AuthorSerializer, association.lazy_association.serializer)
251
277
  when :description
252
- assert_instance_of(PostSerializer::DescriptionSerializer, association.serializer)
278
+ assert_instance_of(PostSerializer::DescriptionSerializer, association.lazy_association.serializer)
253
279
  else
254
280
  flunk "Unknown association: #{key}"
255
281
  end
@@ -260,7 +286,7 @@ module ActiveModel
260
286
  def test_conditional_associations
261
287
  model = Class.new(::Model) do
262
288
  attributes :true, :false
263
- associations :association
289
+ associations :something
264
290
  end.new(true: true, false: false)
265
291
 
266
292
  scenarios = [
@@ -284,7 +310,7 @@ module ActiveModel
284
310
 
285
311
  scenarios.each do |s|
286
312
  serializer = Class.new(ActiveModel::Serializer) do
287
- belongs_to :association, s[:options]
313
+ belongs_to :something, s[:options]
288
314
 
289
315
  def true
290
316
  true
@@ -296,7 +322,7 @@ module ActiveModel
296
322
  end
297
323
 
298
324
  hash = serializable(model, serializer: serializer).serializable_hash
299
- assert_equal(s[:included], hash.key?(:association), "Error with #{s[:options]}")
325
+ assert_equal(s[:included], hash.key?(:something), "Error with #{s[:options]}")
300
326
  end
301
327
  end
302
328
 
@@ -341,8 +367,8 @@ module ActiveModel
341
367
  @author_serializer = AuthorSerializer.new(@author)
342
368
  @inherited_post_serializer = InheritedPostSerializer.new(@post)
343
369
  @inherited_author_serializer = InheritedAuthorSerializer.new(@author)
344
- @author_associations = @author_serializer.associations.to_a
345
- @inherited_author_associations = @inherited_author_serializer.associations.to_a
370
+ @author_associations = @author_serializer.associations.to_a.sort_by(&:name)
371
+ @inherited_author_associations = @inherited_author_serializer.associations.to_a.sort_by(&:name)
346
372
  @post_associations = @post_serializer.associations.to_a
347
373
  @inherited_post_associations = @inherited_post_serializer.associations.to_a
348
374
  end
@@ -361,28 +387,35 @@ module ActiveModel
361
387
 
362
388
  test 'a serializer inheriting from another serializer can redefine has_many and has_one associations' do
363
389
  expected = [:roles, :bio].sort
364
- result = (@inherited_author_associations - @author_associations).map(&:name).sort
390
+ result = (@inherited_author_associations.map(&:reflection) - @author_associations.map(&:reflection)).map(&:name)
365
391
  assert_equal(result, expected)
392
+ assert_equal [true, false, true], @inherited_author_associations.map(&:polymorphic?)
393
+ assert_equal [false, false, false], @author_associations.map(&:polymorphic?)
366
394
  end
367
395
 
368
396
  test 'a serializer inheriting from another serializer can redefine belongs_to associations' do
369
397
  assert_equal [:author, :comments, :blog], @post_associations.map(&:name)
370
398
  assert_equal [:author, :comments, :blog, :comments], @inherited_post_associations.map(&:name)
371
399
 
372
- refute @post_associations.detect { |assoc| assoc.name == :author }.options.key?(:polymorphic)
373
- assert_equal true, @inherited_post_associations.detect { |assoc| assoc.name == :author }.options.fetch(:polymorphic)
400
+ refute @post_associations.detect { |assoc| assoc.name == :author }.polymorphic?
401
+ assert @inherited_post_associations.detect { |assoc| assoc.name == :author }.polymorphic?
374
402
 
375
- refute @post_associations.detect { |assoc| assoc.name == :comments }.options.key?(:key)
403
+ refute @post_associations.detect { |assoc| assoc.name == :comments }.key?
376
404
  original_comment_assoc, new_comments_assoc = @inherited_post_associations.select { |assoc| assoc.name == :comments }
377
- refute original_comment_assoc.options.key?(:key)
378
- assert_equal :reviews, new_comments_assoc.options.fetch(:key)
379
-
380
- assert_equal @post_associations.detect { |assoc| assoc.name == :blog }, @inherited_post_associations.detect { |assoc| assoc.name == :blog }
405
+ refute original_comment_assoc.key?
406
+ assert_equal :reviews, new_comments_assoc.key
407
+
408
+ original_blog = @post_associations.detect { |assoc| assoc.name == :blog }
409
+ inherited_blog = @inherited_post_associations.detect { |assoc| assoc.name == :blog }
410
+ original_parent_serializer = original_blog.lazy_association.association_options.delete(:parent_serializer)
411
+ inherited_parent_serializer = inherited_blog.lazy_association.association_options.delete(:parent_serializer)
412
+ assert_equal PostSerializer, original_parent_serializer.class
413
+ assert_equal InheritedPostSerializer, inherited_parent_serializer.class
381
414
  end
382
415
 
383
416
  test 'a serializer inheriting from another serializer can have an additional association with the same name but with different key' do
384
417
  expected = [:author, :comments, :blog, :reviews].sort
385
- result = @inherited_post_serializer.associations.map { |a| a.options.fetch(:key, a.name) }.sort
418
+ result = @inherited_post_serializer.associations.map(&:key).sort
386
419
  assert_equal(result, expected)
387
420
  end
388
421
  end