Skip to content

Commit 0df26d0

Browse files
committed
Make test attributes explicit
1 parent 93bbc59 commit 0df26d0

File tree

14 files changed

+140
-93
lines changed

14 files changed

+140
-93
lines changed

lib/active_model_serializers/model.rb

Lines changed: 63 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -3,42 +3,50 @@
33
# serializable non-activerecord objects.
44
module ActiveModelSerializers
55
class Model
6-
include ActiveModel::Model
76
include ActiveModel::Serializers::JSON
7+
include ActiveModel::Validations
8+
include ActiveModel::Conversion
9+
extend ActiveModel::Naming
10+
extend ActiveModel::Translation
11+
12+
class_attribute :attribute_names
13+
self.attribute_names = []
814

915
def self.attributes(*names)
1016
attr_accessor(*names)
17+
self.attribute_names = attribute_names | names.map(&:to_sym)
1118
end
1219

13-
attr_reader :attributes, :errors
14-
15-
def initialize(attributes = {})
16-
@attributes = attributes && attributes.symbolize_keys
17-
@errors = ActiveModel::Errors.new(self)
18-
super
19-
end
20+
attributes :id
21+
attr_writer :updated_at
2022

2123
# Defaults to the downcased model name.
2224
def id
23-
attributes.fetch(:id) { self.class.name.downcase }
25+
@id ||= self.class.name.downcase
2426
end
2527

2628
# Defaults to the downcased model name and updated_at
2729
def cache_key
28-
attributes.fetch(:cache_key) { "#{self.class.name.downcase}/#{id}-#{updated_at.strftime('%Y%m%d%H%M%S%9N')}" }
30+
"#{self.class.name.downcase}/#{id}-#{updated_at.strftime('%Y%m%d%H%M%S%9N')}"
2931
end
3032

3133
# Defaults to the time the serializer file was modified.
3234
def updated_at
33-
attributes.fetch(:updated_at) { File.mtime(__FILE__) }
35+
defined?(@updated_at) ? @updated_at : File.mtime(__FILE__)
3436
end
3537

36-
def read_attribute_for_serialization(key)
37-
if key == :id || key == 'id'
38-
attributes.fetch(key) { id }
39-
else
40-
attributes[key]
41-
end
38+
attr_reader :errors
39+
40+
def initialize(attributes = {})
41+
assign_attributes(attributes) if attributes
42+
@errors = ActiveModel::Errors.new(self)
43+
super()
44+
end
45+
46+
def attributes
47+
attribute_names.each_with_object({}) do |attribute_name, result|
48+
result[attribute_name] = public_send(attribute_name)
49+
end.with_indifferent_access
4250
end
4351

4452
# The following methods are needed to be minimally implemented for ActiveModel::Errors
@@ -51,5 +59,43 @@ def self.lookup_ancestors
5159
[self]
5260
end
5361
# :nocov:
62+
63+
def assign_attributes(new_attributes)
64+
unless new_attributes.respond_to?(:stringify_keys)
65+
fail ArgumentError, 'When assigning attributes, you must pass a hash as an argument.'
66+
end
67+
return if new_attributes.blank?
68+
69+
attributes = new_attributes.stringify_keys
70+
_assign_attributes(attributes)
71+
end
72+
73+
private
74+
75+
def _assign_attributes(attributes)
76+
attributes.each do |k, v|
77+
_assign_attribute(k, v)
78+
end
79+
end
80+
81+
def _assign_attribute(k, v)
82+
fail UnknownAttributeError.new(self, k) unless respond_to?("#{k}=")
83+
public_send("#{k}=", v)
84+
end
85+
86+
def persisted?
87+
false
88+
end
89+
90+
# Raised when unknown attributes are supplied via mass assignment.
91+
class UnknownAttributeError < NoMethodError
92+
attr_reader :record, :attribute
93+
94+
def initialize(record, attribute)
95+
@record = record
96+
@attribute = attribute
97+
super("unknown attribute '#{attribute}' for #{@record.class}.")
98+
end
99+
end
54100
end
55101
end

test/action_controller/adapter_selector_test.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ def render_using_adapter_override
1515
end
1616

1717
def render_skipping_adapter
18-
@profile = Profile.new(name: 'Name 1', description: 'Description 1', comments: 'Comments 1')
18+
@profile = Profile.new(id: 'render_skipping_adapter_id', name: 'Name 1', description: 'Description 1', comments: 'Comments 1')
1919
render json: @profile, adapter: false
2020
end
2121
end
@@ -46,7 +46,7 @@ def test_render_using_adapter_override
4646

4747
def test_render_skipping_adapter
4848
get :render_skipping_adapter
49-
assert_equal '{"name":"Name 1","description":"Description 1","comments":"Comments 1"}', response.body
49+
assert_equal '{"id":"render_skipping_adapter_id","name":"Name 1","description":"Description 1","comments":"Comments 1"}', response.body
5050
end
5151
end
5252
end

test/action_controller/json_api/transform_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@ module Serialization
55
class JsonApi
66
class KeyTransformTest < ActionController::TestCase
77
class KeyTransformTestController < ActionController::Base
8-
class Post < ::Model; end
9-
class Author < ::Model; end
10-
class TopComment < ::Model; end
8+
class Post < ::Model; attributes :title, :body, :author, :top_comments, :publish_at end
9+
class Author < ::Model; attributes :first_name, :last_name end
10+
class TopComment < ::Model; attributes :body, :author, :post end
1111
class PostSerializer < ActiveModel::Serializer
1212
type 'posts'
1313
attributes :title, :body, :publish_at

test/action_controller/namespace_lookup_test.rb

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,9 @@
33
module ActionController
44
module Serialization
55
class NamespaceLookupTest < ActionController::TestCase
6-
class Book < ::Model; end
7-
class Page < ::Model; end
8-
class Chapter < ::Model; end
9-
class Writer < ::Model; end
6+
class Book < ::Model; attributes :title, :body, :writer, :chapters end
7+
class Chapter < ::Model; attributes :title end
8+
class Writer < ::Model; attributes :name end
109

1110
module Api
1211
module V2
@@ -50,7 +49,7 @@ class LookupTestController < ActionController::Base
5049

5150
def implicit_namespaced_serializer
5251
writer = Writer.new(name: 'Bob')
53-
book = Book.new(title: 'New Post', body: 'Body', writer: writer, chapters: [])
52+
book = Book.new(id: 'bookid', title: 'New Post', body: 'Body', writer: writer, chapters: [])
5453

5554
render json: book
5655
end
@@ -93,7 +92,7 @@ def explicit_namespace_as_symbol
9392
end
9493

9594
def invalid_namespace
96-
book = Book.new(title: 'New Post', body: 'Body')
95+
book = Book.new(id: 'invalid_namespace_book_id', title: 'New Post', body: 'Body')
9796

9897
render json: book, namespace: :api_v2
9998
end
@@ -205,7 +204,7 @@ def namespace_set_by_request_headers
205204

206205
assert_serializer ActiveModel::Serializer::Null
207206

208-
expected = { 'title' => 'New Post', 'body' => 'Body' }
207+
expected = { 'id' => 'invalid_namespace_book_id', 'title' => 'New Post', 'body' => 'Body', 'writer' => nil, 'chapters' => nil }
209208
actual = JSON.parse(@response.body)
210209

211210
assert_equal expected, actual

test/adapter/json_api/fields_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ module ActiveModelSerializers
44
module Adapter
55
class JsonApi
66
class FieldsTest < ActiveSupport::TestCase
7-
class Post < ::Model; end
8-
class Author < ::Model; end
9-
class Comment < ::Model; end
7+
class Post < ::Model; attributes :title, :body, :author, :comments end
8+
class Author < ::Model; attributes :name, :birthday end
9+
class Comment < ::Model; attributes :body, :author, :post end
1010

1111
class PostSerializer < ActiveModel::Serializer
1212
type 'posts'

test/adapter/json_api/include_data_if_sideloaded_test.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ class Serializer
55
module Adapter
66
class JsonApi
77
class IncludeParamTest < ActiveSupport::TestCase
8-
IncludeParamAuthor = Class.new(::Model)
8+
IncludeParamAuthor = Class.new(::Model) do
9+
attributes :tags, :posts
10+
end
911

1012
class CustomCommentLoader
1113
def all

test/adapter/json_api/linked_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
require 'test_helper'
22

3-
class NestedPost < ::Model; end
3+
class NestedPost < ::Model; attributes :nested_posts end
44
class NestedPostSerializer < ActiveModel::Serializer
55
has_many :nested_posts
66
end
@@ -301,8 +301,8 @@ def test_nil_link_with_specified_serializer
301301
end
302302

303303
class NoDuplicatesTest < ActiveSupport::TestCase
304-
class Post < ::Model; end
305-
class Author < ::Model; end
304+
class Post < ::Model; attributes :author end
305+
class Author < ::Model; attributes :posts, :roles, :bio end
306306

307307
class PostSerializer < ActiveModel::Serializer
308308
type 'posts'

test/adapter/json_api/links_test.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ module ActiveModelSerializers
44
module Adapter
55
class JsonApi
66
class LinksTest < ActiveSupport::TestCase
7-
class LinkAuthor < ::Model; end
7+
class LinkAuthor < ::Model; attributes :posts end
88
class LinkAuthorSerializer < ActiveModel::Serializer
99
link :self do
1010
href "http://example.com/link_author/#{object.id}"

test/adapter/json_api/transform_test.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ module ActiveModelSerializers
44
module Adapter
55
class JsonApi
66
class KeyCaseTest < ActiveSupport::TestCase
7-
class Post < ::Model; end
8-
class Author < ::Model; end
9-
class Comment < ::Model; end
7+
class Post < ::Model; attributes :title, :body, :publish_at, :author, :comments end
8+
class Author < ::Model; attributes :first_name, :last_name end
9+
class Comment < ::Model; attributes :body, :author, :post end
1010

1111
class PostSerializer < ActiveModel::Serializer
1212
type 'posts'

test/cache_test.rb

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ class UncachedAuthor < Author
3434
end
3535

3636
class Article < ::Model
37+
attributes :title
3738
# To confirm error is raised when cache_key is not set and cache_key option not passed to cache
3839
undef_method :cache_key
3940
end
@@ -48,6 +49,10 @@ class InheritedRoleSerializer < RoleSerializer
4849
attribute :special_attribute
4950
end
5051

52+
class Comment < ::Model
53+
attributes :body, :post, :author
54+
end
55+
5156
setup do
5257
cache_store.clear
5358
@comment = Comment.new(id: 1, body: 'ZOMG A COMMENT')
@@ -271,7 +276,7 @@ def test_a_serializer_rendered_by_two_adapter_returns_differently_fetch_attribut
271276
ended_at: nil,
272277
updated_at: alert.updated_at,
273278
created_at: alert.created_at
274-
}
279+
}.with_indifferent_access
275280
expected_cached_jsonapi_attributes = {
276281
id: '1',
277282
type: 'alerts',
@@ -283,15 +288,15 @@ def test_a_serializer_rendered_by_two_adapter_returns_differently_fetch_attribut
283288
updated_at: alert.updated_at,
284289
created_at: alert.created_at
285290
}
286-
}
291+
}.with_indifferent_access
287292

288293
# Assert attributes are serialized correctly
289294
serializable_alert = serializable(alert, serializer: AlertSerializer, adapter: :attributes)
290-
attributes_serialization = serializable_alert.as_json
295+
attributes_serialization = serializable_alert.as_json.with_indifferent_access
291296
assert_equal expected_fetch_attributes, alert.attributes
292297
assert_equal alert.attributes, attributes_serialization
293298
attributes_cache_key = serializable_alert.adapter.serializer.cache_key(serializable_alert.adapter)
294-
assert_equal attributes_serialization, cache_store.fetch(attributes_cache_key)
299+
assert_equal attributes_serialization, cache_store.fetch(attributes_cache_key).with_indifferent_access
295300

296301
serializable_alert = serializable(alert, serializer: AlertSerializer, adapter: :json_api)
297302
jsonapi_cache_key = serializable_alert.adapter.serializer.cache_key(serializable_alert.adapter)
@@ -303,7 +308,7 @@ def test_a_serializer_rendered_by_two_adapter_returns_differently_fetch_attribut
303308
serializable_alert = serializable(alert, serializer: UncachedAlertSerializer, adapter: :json_api)
304309
assert_equal serializable_alert.as_json, jsonapi_serialization
305310

306-
cached_serialization = cache_store.fetch(jsonapi_cache_key)
311+
cached_serialization = cache_store.fetch(jsonapi_cache_key).with_indifferent_access
307312
assert_equal expected_cached_jsonapi_attributes, cached_serialization
308313
ensure
309314
Object.send(:remove_const, :Alert)
@@ -323,17 +328,26 @@ def test_cache_digest_definition
323328
end
324329

325330
def test_object_cache_keys
331+
class << @comment
332+
def cache_key
333+
"comment/#{id}"
334+
end
335+
end
326336
serializable = ActiveModelSerializers::SerializableResource.new([@comment, @comment])
327337
include_directive = JSONAPI::IncludeDirective.new('*', allow_wildcard: true)
328338

329339
actual = ActiveModel::Serializer.object_cache_keys(serializable.adapter.serializer, serializable.adapter, include_directive)
330340

331341
assert_equal 3, actual.size
332-
assert actual.any? { |key| key == "comment/1/#{serializable.adapter.cache_key}" }
333-
assert actual.any? { |key| key =~ %r{post/post-\d+} }
334-
assert actual.any? { |key| key =~ %r{author/author-\d+} }
342+
expected_key = "comment/1/#{serializable.adapter.cache_key}"
343+
assert actual.any? { |key| key == expected_key }, "actual '#{actual}' should include #{expected_key}"
344+
expected_key = %r{post/post-\d+}
345+
assert actual.any? { |key| key =~ expected_key }, "actual '#{actual}' should match '#{expected_key}'"
346+
expected_key = %r{author/author-\d+}
347+
assert actual.any? { |key| key =~ expected_key }, "actual '#{actual}' should match '#{expected_key}'"
335348
end
336349

350+
# rubocop:disable Metrics/AbcSize
337351
def test_fetch_attributes_from_cache
338352
serializers = ActiveModel::Serializer::CollectionSerializer.new([@comment, @comment])
339353

@@ -344,20 +358,21 @@ def test_fetch_attributes_from_cache
344358
adapter_options = {}
345359
adapter_instance = ActiveModelSerializers::Adapter::Attributes.new(serializers, adapter_options)
346360
serializers.serializable_hash(adapter_options, options, adapter_instance)
347-
cached_attributes = adapter_options.fetch(:cached_attributes)
361+
cached_attributes = adapter_options.fetch(:cached_attributes).with_indifferent_access
348362

349363
include_directive = ActiveModelSerializers.default_include_directive
350-
manual_cached_attributes = ActiveModel::Serializer.cache_read_multi(serializers, adapter_instance, include_directive)
364+
manual_cached_attributes = ActiveModel::Serializer.cache_read_multi(serializers, adapter_instance, include_directive).with_indifferent_access
351365
assert_equal manual_cached_attributes, cached_attributes
352366

353-
assert_equal cached_attributes["#{@comment.cache_key}/#{adapter_instance.cache_key}"], Comment.new(id: 1, body: 'ZOMG A COMMENT').attributes
354-
assert_equal cached_attributes["#{@comment.post.cache_key}/#{adapter_instance.cache_key}"], Post.new(id: 'post', title: 'New Post', body: 'Body').attributes
367+
assert_equal cached_attributes["#{@comment.cache_key}/#{adapter_instance.cache_key}"], Comment.new(id: 1, body: 'ZOMG A COMMENT').attributes.reject { |_, v| v.nil? }
368+
assert_equal cached_attributes["#{@comment.post.cache_key}/#{adapter_instance.cache_key}"], Post.new(id: 'post', title: 'New Post', body: 'Body').attributes.reject { |_, v| v.nil? }
355369

356370
writer = @comment.post.blog.writer
357371
writer_cache_key = writer.cache_key
358-
assert_equal cached_attributes["#{writer_cache_key}/#{adapter_instance.cache_key}"], Author.new(id: 'author', name: 'Joao M. D. Moura').attributes
372+
assert_equal cached_attributes["#{writer_cache_key}/#{adapter_instance.cache_key}"], Author.new(id: 'author', name: 'Joao M. D. Moura').attributes.reject { |_, v| v.nil? }
359373
end
360374
end
375+
# rubocop:enable Metrics/AbcSize
361376

362377
def test_cache_read_multi_with_fragment_cache_enabled
363378
post_serializer = Class.new(ActiveModel::Serializer) do

0 commit comments

Comments
 (0)