Skip to content

Commit c25f2f3

Browse files
committed
Fix model attributes accessors
1 parent 4e6bd61 commit c25f2f3

File tree

9 files changed

+169
-35
lines changed

9 files changed

+169
-35
lines changed

lib/active_model_serializers/model.rb

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,17 @@ class Model
66
include ActiveModel::Serializers::JSON
77
include ActiveModel::Model
88

9+
# Declare names of attributes to be included in +sttributes+ hash.
10+
# Is only available as a class-method since the ActiveModel::Serialization mixin in Rails
11+
# uses an +attribute_names+ local variable, which may conflict if we were to add instance methods here.
12+
#
13+
# @overload attribute_names
14+
# @return [Array<Symbol>]
15+
class_attribute :attribute_names, instance_writer: false, instance_reader: false
16+
# Initialize +attribute_names+ for all subclasses. The array is usually
17+
# mutated in the +attributes+ method, but can be set directly, as well.
18+
self.attribute_names = []
19+
920
# Easily declare instance attributes with setters and getters for each.
1021
#
1122
# All attributes to initialize an instance must have setters.
@@ -25,12 +36,46 @@ class Model
2536
# @param names [Array<String, Symbol>]
2637
# @param name [String, Symbol]
2738
def self.attributes(*names)
39+
self.attribute_names |= names.map(&:to_sym)
2840
# Silence redefinition of methods warnings
2941
ActiveModelSerializers.silence_warnings do
3042
attr_accessor(*names)
3143
end
3244
end
3345

46+
# Opt-in to breaking change
47+
def self.derive_attributes_from_names_and_fix_accessors
48+
unless included_modules.include?(DeriveAttributesFromNamesAndFixAccessors)
49+
prepend(DeriveAttributesFromNamesAndFixAccessors)
50+
end
51+
end
52+
53+
module DeriveAttributesFromNamesAndFixAccessors
54+
def self.included(base)
55+
# NOTE that +id+ will always be in +attributes+.
56+
base.attributes :id
57+
end
58+
59+
# Override the initialize method so that attributes aren't processed.
60+
#
61+
# @param attributes [Hash]
62+
def initialize(attributes = {})
63+
@errors = ActiveModel::Errors.new(self)
64+
super
65+
end
66+
67+
# Override the +attributes+ method so that the hash is derived from +attribute_names+.
68+
#
69+
# The the fields in +attribute_names+ determines the returned hash.
70+
# +attributes+ are returned frozen to prevent any expectations that mutation affects
71+
# the actual values in the model.
72+
def attributes
73+
self.class.attribute_names.each_with_object({}) do |attribute_name, result|
74+
result[attribute_name] = public_send(attribute_name).freeze
75+
end.with_indifferent_access.freeze
76+
end
77+
end
78+
3479
# Support for validation and other ActiveModel::Errors
3580
# @return [ActiveModel::Errors]
3681
attr_reader :errors

test/action_controller/adapter_selector_test.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33
module ActionController
44
module Serialization
55
class AdapterSelectorTest < ActionController::TestCase
6+
class Profile < Model
7+
attributes :id, :name, :description
8+
associations :comments
9+
end
10+
class ProfileSerializer < ActiveModel::Serializer
11+
type 'profiles'
12+
attributes :name, :description
13+
end
14+
615
class AdapterSelectorTestController < ActionController::Base
716
def render_using_default_adapter
817
@profile = Profile.new(name: 'Name 1', description: 'Description 1', comments: 'Comments 1')

test/action_controller/namespace_lookup_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module ActionController
44
module Serialization
55
class NamespaceLookupTest < ActionController::TestCase
66
class Book < ::Model
7-
attributes :title, :body
7+
attributes :id, :title, :body
88
associations :writer, :chapters
99
end
1010
class Chapter < ::Model

test/adapter/json/has_many_test.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ module ActiveModelSerializers
44
module Adapter
55
class Json
66
class HasManyTestTest < ActiveSupport::TestCase
7+
class ModelWithoutSerializer < ::Model
8+
attributes :id, :name
9+
end
10+
711
def setup
812
ActionController::Base.cache_store.clear
913
@author = Author.new(id: 1, name: 'Steve K.')
@@ -16,7 +20,7 @@ def setup
1620
@second_comment.post = @post
1721
@blog = Blog.new(id: 1, name: 'My Blog!!')
1822
@post.blog = @blog
19-
@tag = Tag.new(id: 1, name: '#hash_tag')
23+
@tag = ModelWithoutSerializer.new(id: 1, name: '#hash_tag')
2024
@post.tags = [@tag]
2125
end
2226

@@ -30,7 +34,11 @@ def test_has_many
3034
end
3135

3236
def test_has_many_with_no_serializer
33-
serializer = PostWithTagsSerializer.new(@post)
37+
post_serializer_class = Class.new(ActiveModel::Serializer) do
38+
attributes :id
39+
has_many :tags
40+
end
41+
serializer = post_serializer_class.new(@post)
3442
adapter = ActiveModelSerializers::Adapter::Json.new(serializer)
3543
assert_equal({
3644
id: 42,

test/adapter/json_api/has_many_test.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ module ActiveModelSerializers
44
module Adapter
55
class JsonApi
66
class HasManyTest < ActiveSupport::TestCase
7+
class ModelWithoutSerializer < ::Model
8+
attributes :id, :name
9+
end
10+
711
def setup
812
ActionController::Base.cache_store.clear
913
@author = Author.new(id: 1, name: 'Steve K.')
@@ -26,7 +30,7 @@ def setup
2630
@blog.articles = [@post]
2731
@post.blog = @blog
2832
@post_without_comments.blog = nil
29-
@tag = Tag.new(id: 1, name: '#hash_tag')
33+
@tag = ModelWithoutSerializer.new(id: 1, name: '#hash_tag')
3034
@post.tags = [@tag]
3135
@serializer = PostSerializer.new(@post)
3236
@adapter = ActiveModelSerializers::Adapter::JsonApi.new(@serializer)
@@ -129,7 +133,11 @@ def test_include_type_for_association_when_different_than_name
129133
end
130134

131135
def test_has_many_with_no_serializer
132-
serializer = PostWithTagsSerializer.new(@post)
136+
post_serializer_class = Class.new(ActiveModel::Serializer) do
137+
attributes :id
138+
has_many :tags
139+
end
140+
serializer = post_serializer_class.new(@post)
133141
adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer)
134142

135143
assert_equal({

test/adapter/json_api/include_data_if_sideloaded_test.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,21 @@ def all
1414
[{ foo: 'bar' }]
1515
end
1616
end
17+
class Tag < ::Model
18+
attributes :id, :name
19+
end
1720

1821
class TagSerializer < ActiveModel::Serializer
22+
type 'tags'
1923
attributes :id, :name
2024
end
2125

26+
class PostWithTagsSerializer < ActiveModel::Serializer
27+
type 'posts'
28+
attributes :id
29+
has_many :tags
30+
end
31+
2232
class IncludeParamAuthorSerializer < ActiveModel::Serializer
2333
class_attribute :comment_loader
2434

test/cache_test.rb

Lines changed: 74 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,20 @@
44

55
module ActiveModelSerializers
66
class CacheTest < ActiveSupport::TestCase
7+
class Article < ::Model
8+
attributes :title
9+
# To confirm error is raised when cache_key is not set and cache_key option not passed to cache
10+
undef_method :cache_key
11+
end
12+
class ArticleSerializer < ActiveModel::Serializer
13+
cache only: [:place], skip_digest: true
14+
attributes :title
15+
end
16+
17+
class Author < ::Model
18+
attributes :id, :name
19+
associations :posts, :bio, :roles
20+
end
721
# Instead of a primitive cache key (i.e. a string), this class
822
# returns a list of objects that require to be expanded themselves.
923
class AuthorWithExpandableCacheElements < Author
@@ -27,45 +41,85 @@ def cache_key
2741
]
2842
end
2943
end
30-
3144
class UncachedAuthor < Author
3245
# To confirm cache_key is set using updated_at and cache_key option passed to cache
3346
undef_method :cache_key
3447
end
48+
class AuthorSerializer < ActiveModel::Serializer
49+
cache key: 'writer', skip_digest: true
50+
attributes :id, :name
3551

36-
class Article < ::Model
37-
attributes :title
38-
# To confirm error is raised when cache_key is not set and cache_key option not passed to cache
39-
undef_method :cache_key
52+
has_many :posts
53+
has_many :roles
54+
has_one :bio
4055
end
4156

42-
class ArticleSerializer < ActiveModel::Serializer
43-
cache only: [:place], skip_digest: true
44-
attributes :title
57+
class Blog < ::Model
58+
attributes :name
59+
associations :writer
4560
end
61+
class BlogSerializer < ActiveModel::Serializer
62+
cache key: 'blog'
63+
attributes :id, :name
4664

47-
class InheritedRoleSerializer < RoleSerializer
48-
cache key: 'inherited_role', only: [:name, :special_attribute]
49-
attribute :special_attribute
65+
belongs_to :writer
5066
end
5167

5268
class Comment < ::Model
53-
attributes :body
69+
attributes :id, :body
5470
associations :post, :author
5571

5672
# Uses a custom non-time-based cache key
5773
def cache_key
5874
"comment/#{id}"
5975
end
6076
end
77+
class CommentSerializer < ActiveModel::Serializer
78+
cache expires_in: 1.day, skip_digest: true
79+
attributes :id, :body
80+
belongs_to :post
81+
belongs_to :author
82+
end
83+
84+
class Post < ::Model
85+
attributes :id, :title, :body
86+
associations :author, :comments, :blog
87+
end
88+
class PostSerializer < ActiveModel::Serializer
89+
cache key: 'post', expires_in: 0.1, skip_digest: true
90+
attributes :id, :title, :body
91+
92+
has_many :comments
93+
belongs_to :blog
94+
belongs_to :author
95+
end
96+
97+
class Role < ::Model
98+
attributes :name, :description, :special_attribute
99+
associations :author
100+
end
101+
class RoleSerializer < ActiveModel::Serializer
102+
cache only: [:name, :slug], skip_digest: true
103+
attributes :id, :name, :description
104+
attribute :friendly_id, key: :slug
105+
belongs_to :author
106+
107+
def friendly_id
108+
"#{object.name}-#{object.id}"
109+
end
110+
end
111+
class InheritedRoleSerializer < RoleSerializer
112+
cache key: 'inherited_role', only: [:name, :special_attribute]
113+
attribute :special_attribute
114+
end
61115

62116
setup do
63117
cache_store.clear
64118
@comment = Comment.new(id: 1, body: 'ZOMG A COMMENT')
65-
@post = Post.new(title: 'New Post', body: 'Body')
119+
@post = Post.new(id: 'post', title: 'New Post', body: 'Body')
66120
@bio = Bio.new(id: 1, content: 'AMS Contributor')
67-
@author = Author.new(name: 'Joao M. D. Moura')
68-
@blog = Blog.new(id: 999, name: 'Custom blog', writer: @author, articles: [])
121+
@author = Author.new(id: 'author', name: 'Joao M. D. Moura')
122+
@blog = Blog.new(id: 999, name: 'Custom blog', writer: @author)
69123
@role = Role.new(name: 'Great Author')
70124
@location = Location.new(lat: '-23.550520', lng: '-46.633309')
71125
@place = Place.new(name: 'Amazing Place')
@@ -325,12 +379,14 @@ def test_a_serializer_rendered_by_two_adapter_returns_differently_fetch_attribut
325379

326380
def test_uses_file_digest_in_cache_key
327381
render_object_with_cache(@blog)
328-
key = "#{@blog.cache_key}/#{adapter.cache_key}/#{::Model::FILE_DIGEST}"
382+
file_digest = Digest::MD5.hexdigest(File.open(__FILE__).read)
383+
key = "#{@blog.cache_key}/#{adapter.cache_key}/#{file_digest}"
329384
assert_equal(@blog_serializer.attributes, cache_store.fetch(key))
330385
end
331386

332387
def test_cache_digest_definition
333-
assert_equal(::Model::FILE_DIGEST, @post_serializer.class._cache_digest)
388+
file_digest = Digest::MD5.hexdigest(File.open(__FILE__).read)
389+
assert_equal(file_digest, @post_serializer.class._cache_digest)
334390
end
335391

336392
def test_object_cache_keys
@@ -532,7 +588,7 @@ def test_fragment_fetch_with_virtual_attributes
532588
role_hash = role_serializer.fetch_attributes_fragment(adapter_instance)
533589
assert_equal(role_hash, expected_result)
534590

535-
role.attributes[:id] = 'this has been updated'
591+
role.id = 'this has been updated'
536592
role.name = 'this was cached'
537593

538594
role_hash = role_serializer.fetch_attributes_fragment(adapter_instance)

test/fixtures/poro.rb

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
class Model < ActiveModelSerializers::Model
2-
FILE_DIGEST = Digest::MD5.hexdigest(File.open(__FILE__).read)
3-
42
attr_writer :id
53

64
# At this time, just for organization of intent
@@ -108,10 +106,6 @@ class PostPreviewSerializer < ActiveModel::Serializer
108106
has_many :comments, serializer: ::CommentPreviewSerializer
109107
belongs_to :author, serializer: ::AuthorPreviewSerializer
110108
end
111-
class PostWithTagsSerializer < ActiveModel::Serializer
112-
attributes :id
113-
has_many :tags
114-
end
115109
class PostWithCustomKeysSerializer < ActiveModel::Serializer
116110
attributes :id
117111
has_many :comments, key: :reviews
@@ -208,10 +202,6 @@ class UnrelatedLinkSerializer < ActiveModel::Serializer
208202
end
209203
end
210204

211-
class Tag < Model
212-
attributes :name
213-
end
214-
215205
class VirtualValue < Model; end
216206
class VirtualValueSerializer < ActiveModel::Serializer
217207
attributes :id

test/serializers/associations_test.rb

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22
module ActiveModel
33
class Serializer
44
class AssociationsTest < ActiveSupport::TestCase
5+
class ModelWithoutSerializer < ::Model
6+
attributes :id, :name
7+
end
8+
59
def setup
610
@author = Author.new(name: 'Steve K.')
711
@author.bio = nil
812
@author.roles = []
913
@blog = Blog.new(name: 'AMS Blog')
1014
@post = Post.new(title: 'New Post', body: 'Body')
11-
@tag = Tag.new(id: 'tagid', name: '#hashtagged')
15+
@tag = ModelWithoutSerializer.new(id: 'tagid', name: '#hashtagged')
1216
@comment = Comment.new(id: 1, body: 'ZOMG A COMMENT')
1317
@post.comments = [@comment]
1418
@post.tags = [@tag]
@@ -46,7 +50,11 @@ def test_has_many_and_has_one
4650
end
4751

4852
def test_has_many_with_no_serializer
49-
PostWithTagsSerializer.new(@post).associations.each do |association|
53+
post_serializer_class = Class.new(ActiveModel::Serializer) do
54+
attributes :id
55+
has_many :tags
56+
end
57+
post_serializer_class.new(@post).associations.each do |association|
5058
key = association.key
5159
serializer = association.serializer
5260
options = association.options

0 commit comments

Comments
 (0)