diff --git a/CHANGELOG.md b/CHANGELOG.md index dfc0d0792..b025a61bb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,16 +6,16 @@ Breaking changes: Features: -- [#1982](https://github.com/rails-api/active_model_serializers/pull/1982) Add ActiveModelSerializers::Model.attributes to configure PORO attributes. (@bf4) +- [#2021](https://github.com/rails-api/active_model_serializers/pull/2021) ActiveModelSerializers::Model#attributes. Originally in [#1982](https://github.com/rails-api/active_model_serializers/pull/1982). (@bf4) Fixes: -- [#1984](https://github.com/rails-api/active_model_serializers/pull/1984) Mutation of ActiveModelSerializers::Model now changes the attributes. (@bf4) +- [#2022](https://github.com/rails-api/active_model_serializers/pull/2022) Mutation of ActiveModelSerializers::Model now changes the attributes. Originally in [#1984](https://github.com/rails-api/active_model_serializers/pull/1984). (@bf4) Misc: +- [#2021](https://github.com/rails-api/active_model_serializers/pull/2021) Make test attributes explicit. Tests have Model#associations. (@bf4) - [#1981](https://github.com/rails-api/active_model_serializers/pull/1981) Fix relationship link documentation. (@groyoh) -- [#1984](https://github.com/rails-api/active_model_serializers/pull/1984) Make test attributes explicit. Test models have 'associations' support. (@bf4) ### [v0.10.4 (2017-01-06)](https://github.com/rails-api/active_model_serializers/compare/v0.10.3...v0.10.4) diff --git a/docs/howto/serialize_poro.md b/docs/howto/serialize_poro.md index fa9c9bf84..20091c52a 100644 --- a/docs/howto/serialize_poro.md +++ b/docs/howto/serialize_poro.md @@ -2,13 +2,16 @@ # How to serialize a Plain-Old Ruby Object (PORO) -When you are first getting started with ActiveModelSerializers, it may seem only `ActiveRecord::Base` objects can be serializable, but pretty much any object can be serializable with ActiveModelSerializers. Here is an example of a PORO that is serializable: +When you are first getting started with ActiveModelSerializers, it may seem only `ActiveRecord::Base` objects can be serializable, +but pretty much any object can be serializable with ActiveModelSerializers. +Here is an example of a PORO that is serializable in most situations: + ```ruby # my_model.rb class MyModel alias :read_attribute_for_serialization :send attr_accessor :id, :name, :level - + def initialize(attributes) @id = attributes[:id] @name = attributes[:name] @@ -21,7 +24,15 @@ class MyModel end ``` -Fortunately, ActiveModelSerializers provides a [`ActiveModelSerializers::Model`](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model_serializers/model.rb) which you can use in production code that will make your PORO a lot cleaner. The above code now becomes: +The [ActiveModel::Serializer::Lint::Tests](../../lib/active_model/serializer/lint.rb) +define and validate which methods ActiveModelSerializers expects to be implemented. + +An implementation of the complete spec is included either for use or as reference: +[`ActiveModelSerializers::Model`](../../lib/active_model_serializers/model.rb). +You can use in production code that will make your PORO a lot cleaner. + +The above code now becomes: + ```ruby # my_model.rb class MyModel < ActiveModelSerializers::Model @@ -29,4 +40,6 @@ class MyModel < ActiveModelSerializers::Model end ``` -The default serializer would be `MyModelSerializer`. \ No newline at end of file +The default serializer would be `MyModelSerializer`. + +For more information, see [README: What does a 'serializable resource' look like?](../../README.md#what-does-a-serializable-resource-look-like). diff --git a/lib/active_model_serializers/model.rb b/lib/active_model_serializers/model.rb index 0bdeda21b..b61661bc1 100644 --- a/lib/active_model_serializers/model.rb +++ b/lib/active_model_serializers/model.rb @@ -1,16 +1,40 @@ -# ActiveModelSerializers::Model is a convenient -# serializable class to inherit from when making -# serializable non-activerecord objects. +# ActiveModelSerializers::Model is a convenient superclass for making your models +# from Plain-Old Ruby Objects (PORO). It also serves as a reference implementation +# that satisfies ActiveModel::Serializer::Lint::Tests. module ActiveModelSerializers class Model include ActiveModel::Serializers::JSON include ActiveModel::Model - class_attribute :attribute_names + # Declare names of attributes to be included in +sttributes+ hash. + # Is only available as a class-method since the ActiveModel::Serialization mixin in Rails + # uses an +attribute_names+ local variable, which may conflict if we were to add instance methods here. + # + # @overload attribute_names + # @return [Array] + class_attribute :attribute_names, instance_writer: false, instance_reader: false # Initialize +attribute_names+ for all subclasses. The array is usually # mutated in the +attributes+ method, but can be set directly, as well. self.attribute_names = [] + # Easily declare instance attributes with setters and getters for each. + # + # All attributes to initialize an instance must have setters. + # However, the hash turned by +attributes+ instance method will ALWAYS + # be the value of the initial attributes, regardless of what accessors are defined. + # The only way to change the change the attributes after initialization is + # to mutate the +attributes+ directly. + # Accessor methods do NOT mutate the attributes. (This is a bug). + # + # @note For now, the Model only supports the notion of 'attributes'. + # In the tests, there is a special Model that also supports 'associations'. This is + # important so that we can add accessors for values that should not appear in the + # attributes hash when modeling associations. It is not yet clear if it + # makes sense for a PORO to have associations outside of the tests. + # + # @overload attributes(names) + # @param names [Array] + # @param name [String, Symbol] def self.attributes(*names) self.attribute_names |= names.map(&:to_sym) # Silence redefinition of methods warnings @@ -19,31 +43,90 @@ def self.attributes(*names) end end + # Opt-in to breaking change + def self.derive_attributes_from_names_and_fix_accessors + unless included_modules.include?(DeriveAttributesFromNamesAndFixAccessors) + prepend(DeriveAttributesFromNamesAndFixAccessors) + end + end + + module DeriveAttributesFromNamesAndFixAccessors + def self.included(base) + # NOTE that +id+ will always be in +attributes+. + base.attributes :id + end + + # Override the initialize method so that attributes aren't processed. + # + # @param attributes [Hash] + def initialize(attributes = {}) + @errors = ActiveModel::Errors.new(self) + super + end + + # Override the +attributes+ method so that the hash is derived from +attribute_names+. + # + # The the fields in +attribute_names+ determines the returned hash. + # +attributes+ are returned frozen to prevent any expectations that mutation affects + # the actual values in the model. + def attributes + self.class.attribute_names.each_with_object({}) do |attribute_name, result| + result[attribute_name] = public_send(attribute_name).freeze + end.with_indifferent_access.freeze + end + end + + # Support for validation and other ActiveModel::Errors + # @return [ActiveModel::Errors] attr_reader :errors - # NOTE that +updated_at+ isn't included in +attribute_names+, - # which means it won't show up in +attributes+ unless a subclass has - # either attributes :updated_at which will redefine the methods - # or attribute_names << :updated_at. + + # (see #updated_at) attr_writer :updated_at - # NOTE that +id+ will always be in +attributes+. - attributes :id + # The only way to change the attributes of an instance is to directly mutate the attributes. + # @example + # + # model.attributes[:foo] = :bar + # @return [Hash] + attr_reader :attributes + + # @param attributes [Hash] def initialize(attributes = {}) + attributes ||= {} # protect against nil + @attributes = attributes.symbolize_keys.with_indifferent_access @errors = ActiveModel::Errors.new(self) super end - # The the fields in +attribute_names+ determines the returned hash. - # +attributes+ are returned frozen to prevent any expectations that mutation affects - # the actual values in the model. - def attributes - attribute_names.each_with_object({}) do |attribute_name, result| - result[attribute_name] = public_send(attribute_name).freeze - end.with_indifferent_access.freeze + # Defaults to the downcased model name. + # This probably isn't a good default, since it's not a unique instance identifier, + # but that's what is currently implemented \_('-')_/. + # + # @note Though +id+ is defined, it will only show up + # in +attributes+ when it is passed in to the initializer or added to +attributes+, + # such as attributes[:id] = 5. + # @return [String, Numeric, Symbol] + def id + attributes.fetch(:id) do + defined?(@id) ? @id : self.class.model_name.name && self.class.model_name.name.downcase + end + end + + # When not set, defaults to the time the file was modified. + # + # @note Though +updated_at+ and +updated_at=+ are defined, it will only show up + # in +attributes+ when it is passed in to the initializer or added to +attributes+, + # such as attributes[:updated_at] = Time.current. + # @return [String, Numeric, Time] + def updated_at + attributes.fetch(:updated_at) do + defined?(@updated_at) ? @updated_at : File.mtime(__FILE__) + end end # To customize model behavior, this method must be redefined. However, # there are other ways of setting the +cache_key+ a serializer uses. + # @return [String] def cache_key ActiveSupport::Cache.expand_cache_key([ self.class.model_name.name.downcase, @@ -51,12 +134,6 @@ def cache_key ].compact) end - # When no set, defaults to the time the file was modified. - # See NOTE by attr_writer :updated_at - def updated_at - defined?(@updated_at) ? @updated_at : File.mtime(__FILE__) - end - # The following methods are needed to be minimally implemented for ActiveModel::Errors # :nocov: def self.human_attribute_name(attr, _options = {}) diff --git a/test/action_controller/adapter_selector_test.rb b/test/action_controller/adapter_selector_test.rb index 6f22aae25..db93573b4 100644 --- a/test/action_controller/adapter_selector_test.rb +++ b/test/action_controller/adapter_selector_test.rb @@ -3,6 +3,15 @@ module ActionController module Serialization class AdapterSelectorTest < ActionController::TestCase + class Profile < Model + attributes :id, :name, :description + associations :comments + end + class ProfileSerializer < ActiveModel::Serializer + type 'profiles' + attributes :name, :description + end + class AdapterSelectorTestController < ActionController::Base def render_using_default_adapter @profile = Profile.new(name: 'Name 1', description: 'Description 1', comments: 'Comments 1') diff --git a/test/action_controller/namespace_lookup_test.rb b/test/action_controller/namespace_lookup_test.rb index f40cca11d..b5c8f496d 100644 --- a/test/action_controller/namespace_lookup_test.rb +++ b/test/action_controller/namespace_lookup_test.rb @@ -4,7 +4,7 @@ module ActionController module Serialization class NamespaceLookupTest < ActionController::TestCase class Book < ::Model - attributes :title, :body + attributes :id, :title, :body associations :writer, :chapters end class Chapter < ::Model @@ -86,7 +86,7 @@ def explicit_namespace_as_string book = Book.new(title: 'New Post', body: 'Body') # because this is a string, ruby can't auto-lookup the constant, so otherwise - # the looku things we mean ::Api::V2 + # the lookup thinks we mean ::Api::V2 render json: book, namespace: 'ActionController::Serialization::NamespaceLookupTest::Api::V2' end @@ -94,7 +94,7 @@ def explicit_namespace_as_symbol book = Book.new(title: 'New Post', body: 'Body') # because this is a string, ruby can't auto-lookup the constant, so otherwise - # the looku things we mean ::Api::V2 + # the lookup thinks we mean ::Api::V2 render json: book, namespace: :'ActionController::Serialization::NamespaceLookupTest::Api::V2' end diff --git a/test/active_model_serializers/model_test.rb b/test/active_model_serializers/model_test.rb index c2f316479..6a8a29afb 100644 --- a/test/active_model_serializers/model_test.rb +++ b/test/active_model_serializers/model_test.rb @@ -24,21 +24,56 @@ def test_attributes_can_be_read_for_serialization attributes :one, :two, :three end original_attributes = { one: 1, two: 2, three: 3 } - instance = klass.new(original_attributes) + original_instance = klass.new(original_attributes) # Initial value - expected_attributes = { id: nil, one: 1, two: 2, three: 3 }.with_indifferent_access + instance = original_instance + expected_attributes = { one: 1, two: 2, three: 3 }.with_indifferent_access assert_equal expected_attributes, instance.attributes assert_equal 1, instance.one assert_equal 1, instance.read_attribute_for_serialization(:one) - # Change via accessor + # FIXME: Change via accessor has no effect on attributes. + instance = original_instance.dup instance.one = :not_one + assert_equal expected_attributes, instance.attributes + assert_equal :not_one, instance.one + assert_equal :not_one, instance.read_attribute_for_serialization(:one) + + # FIXME: Change via mutating attributes + instance = original_instance.dup + instance.attributes[:one] = :not_one + expected_attributes = { one: :not_one, two: 2, three: 3 }.with_indifferent_access + assert_equal expected_attributes, instance.attributes + assert_equal 1, instance.one + assert_equal 1, instance.read_attribute_for_serialization(:one) + end + + def test_attributes_can_be_read_for_serialization_with_attributes_accessors_fix + klass = Class.new(ActiveModelSerializers::Model) do + derive_attributes_from_names_and_fix_accessors + attributes :one, :two, :three + end + original_attributes = { one: 1, two: 2, three: 3 } + original_instance = klass.new(original_attributes) + + # Initial value + instance = original_instance + expected_attributes = { one: 1, two: 2, three: 3 }.with_indifferent_access + assert_equal expected_attributes, instance.attributes + assert_equal 1, instance.one + assert_equal 1, instance.read_attribute_for_serialization(:one) - expected_attributes = { id: nil, one: :not_one, two: 2, three: 3 }.with_indifferent_access + expected_attributes = { one: :not_one, two: 2, three: 3 }.with_indifferent_access + # Change via accessor + instance = original_instance.dup + instance.one = :not_one assert_equal expected_attributes, instance.attributes assert_equal :not_one, instance.one assert_equal :not_one, instance.read_attribute_for_serialization(:one) + + # Attributes frozen + assert instance.attributes.frozen? end def test_id_attribute_can_be_read_for_serialization @@ -47,21 +82,59 @@ def test_id_attribute_can_be_read_for_serialization end self.class.const_set(:SomeTestModel, klass) original_attributes = { id: :ego, one: 1, two: 2, three: 3 } - instance = klass.new(original_attributes) + original_instance = klass.new(original_attributes) # Initial value + instance = original_instance.dup expected_attributes = { id: :ego, one: 1, two: 2, three: 3 }.with_indifferent_access assert_equal expected_attributes, instance.attributes - assert_equal 1, instance.one - assert_equal 1, instance.read_attribute_for_serialization(:one) + assert_equal :ego, instance.id + assert_equal :ego, instance.read_attribute_for_serialization(:id) - # Change via accessor + # FIXME: Change via accessor has no effect on attributes. + instance = original_instance.dup instance.id = :superego + assert_equal expected_attributes, instance.attributes + assert_equal :superego, instance.id + assert_equal :superego, instance.read_attribute_for_serialization(:id) + # FIXME: Change via mutating attributes + instance = original_instance.dup + instance.attributes[:id] = :superego expected_attributes = { id: :superego, one: 1, two: 2, three: 3 }.with_indifferent_access assert_equal expected_attributes, instance.attributes + assert_equal :ego, instance.id + assert_equal :ego, instance.read_attribute_for_serialization(:id) + ensure + self.class.send(:remove_const, :SomeTestModel) + end + + def test_id_attribute_can_be_read_for_serialization_with_attributes_accessors_fix + klass = Class.new(ActiveModelSerializers::Model) do + derive_attributes_from_names_and_fix_accessors + attributes :id, :one, :two, :three + end + self.class.const_set(:SomeTestModel, klass) + original_attributes = { id: :ego, one: 1, two: 2, three: 3 } + original_instance = klass.new(original_attributes) + + # Initial value + instance = original_instance.dup + expected_attributes = { id: :ego, one: 1, two: 2, three: 3 }.with_indifferent_access + assert_equal expected_attributes, instance.attributes + assert_equal :ego, instance.id + assert_equal :ego, instance.read_attribute_for_serialization(:id) + + expected_attributes = { id: :superego, one: 1, two: 2, three: 3 }.with_indifferent_access + # Change via accessor + instance = original_instance.dup + instance.id = :superego + assert_equal expected_attributes, instance.attributes assert_equal :superego, instance.id assert_equal :superego, instance.read_attribute_for_serialization(:id) + + # Attributes frozen + assert instance.attributes.frozen? ensure self.class.send(:remove_const, :SomeTestModel) end diff --git a/test/active_model_serializers/register_jsonapi_renderer_test_isolated.rb b/test/active_model_serializers/register_jsonapi_renderer_test_isolated.rb index 37452955f..30542408f 100644 --- a/test/active_model_serializers/register_jsonapi_renderer_test_isolated.rb +++ b/test/active_model_serializers/register_jsonapi_renderer_test_isolated.rb @@ -44,7 +44,7 @@ def assert_parses(expected, actual, headers = {}) def define_author_model_and_serializer TestController.const_set(:Author, Class.new(ActiveModelSerializers::Model) do - attributes :name + attributes :id, :name end) TestController.const_set(:AuthorSerializer, Class.new(ActiveModel::Serializer) do type 'users' diff --git a/test/adapter/attributes_test.rb b/test/adapter/attributes_test.rb index dee0c7753..e60019f50 100644 --- a/test/adapter/attributes_test.rb +++ b/test/adapter/attributes_test.rb @@ -3,11 +3,8 @@ module ActiveModelSerializers module Adapter class AttributesTest < ActiveSupport::TestCase - class Person - include ActiveModel::Model - include ActiveModel::Serialization - - attr_accessor :first_name, :last_name + class Person < ActiveModelSerializers::Model + attributes :first_name, :last_name end class PersonSerializer < ActiveModel::Serializer diff --git a/test/adapter/json/has_many_test.rb b/test/adapter/json/has_many_test.rb index 3f6fa546e..feeec93c3 100644 --- a/test/adapter/json/has_many_test.rb +++ b/test/adapter/json/has_many_test.rb @@ -4,6 +4,10 @@ module ActiveModelSerializers module Adapter class Json class HasManyTestTest < ActiveSupport::TestCase + class ModelWithoutSerializer < ::Model + attributes :id, :name + end + def setup ActionController::Base.cache_store.clear @author = Author.new(id: 1, name: 'Steve K.') @@ -16,7 +20,7 @@ def setup @second_comment.post = @post @blog = Blog.new(id: 1, name: 'My Blog!!') @post.blog = @blog - @tag = Tag.new(id: 1, name: '#hash_tag') + @tag = ModelWithoutSerializer.new(id: 1, name: '#hash_tag') @post.tags = [@tag] end @@ -30,7 +34,11 @@ def test_has_many end def test_has_many_with_no_serializer - serializer = PostWithTagsSerializer.new(@post) + post_serializer_class = Class.new(ActiveModel::Serializer) do + attributes :id + has_many :tags + end + serializer = post_serializer_class.new(@post) adapter = ActiveModelSerializers::Adapter::Json.new(serializer) assert_equal({ id: 42, diff --git a/test/adapter/json_api/has_many_test.rb b/test/adapter/json_api/has_many_test.rb index 9da73af98..a9fa9ac92 100644 --- a/test/adapter/json_api/has_many_test.rb +++ b/test/adapter/json_api/has_many_test.rb @@ -4,6 +4,10 @@ module ActiveModelSerializers module Adapter class JsonApi class HasManyTest < ActiveSupport::TestCase + class ModelWithoutSerializer < ::Model + attributes :id, :name + end + def setup ActionController::Base.cache_store.clear @author = Author.new(id: 1, name: 'Steve K.') @@ -26,7 +30,7 @@ def setup @blog.articles = [@post] @post.blog = @blog @post_without_comments.blog = nil - @tag = Tag.new(id: 1, name: '#hash_tag') + @tag = ModelWithoutSerializer.new(id: 1, name: '#hash_tag') @post.tags = [@tag] @serializer = PostSerializer.new(@post) @adapter = ActiveModelSerializers::Adapter::JsonApi.new(@serializer) @@ -129,7 +133,11 @@ def test_include_type_for_association_when_different_than_name end def test_has_many_with_no_serializer - serializer = PostWithTagsSerializer.new(@post) + post_serializer_class = Class.new(ActiveModel::Serializer) do + attributes :id + has_many :tags + end + serializer = post_serializer_class.new(@post) adapter = ActiveModelSerializers::Adapter::JsonApi.new(serializer) assert_equal({ diff --git a/test/adapter/json_api/include_data_if_sideloaded_test.rb b/test/adapter/json_api/include_data_if_sideloaded_test.rb index b5c818693..c0da94886 100644 --- a/test/adapter/json_api/include_data_if_sideloaded_test.rb +++ b/test/adapter/json_api/include_data_if_sideloaded_test.rb @@ -14,11 +14,21 @@ def all [{ foo: 'bar' }] end end + class Tag < ::Model + attributes :id, :name + end class TagSerializer < ActiveModel::Serializer + type 'tags' attributes :id, :name end + class PostWithTagsSerializer < ActiveModel::Serializer + type 'posts' + attributes :id + has_many :tags + end + class IncludeParamAuthorSerializer < ActiveModel::Serializer class_attribute :comment_loader diff --git a/test/cache_test.rb b/test/cache_test.rb index 06043fb7f..f09589314 100644 --- a/test/cache_test.rb +++ b/test/cache_test.rb @@ -4,6 +4,20 @@ module ActiveModelSerializers class CacheTest < ActiveSupport::TestCase + class Article < ::Model + attributes :title + # To confirm error is raised when cache_key is not set and cache_key option not passed to cache + undef_method :cache_key + end + class ArticleSerializer < ActiveModel::Serializer + cache only: [:place], skip_digest: true + attributes :title + end + + class Author < ::Model + attributes :id, :name + associations :posts, :bio, :roles + end # Instead of a primitive cache key (i.e. a string), this class # returns a list of objects that require to be expanded themselves. class AuthorWithExpandableCacheElements < Author @@ -27,30 +41,32 @@ def cache_key ] end end - class UncachedAuthor < Author # To confirm cache_key is set using updated_at and cache_key option passed to cache undef_method :cache_key end + class AuthorSerializer < ActiveModel::Serializer + cache key: 'writer', skip_digest: true + attributes :id, :name - class Article < ::Model - attributes :title - # To confirm error is raised when cache_key is not set and cache_key option not passed to cache - undef_method :cache_key + has_many :posts + has_many :roles + has_one :bio end - class ArticleSerializer < ActiveModel::Serializer - cache only: [:place], skip_digest: true - attributes :title + class Blog < ::Model + attributes :name + associations :writer end + class BlogSerializer < ActiveModel::Serializer + cache key: 'blog' + attributes :id, :name - class InheritedRoleSerializer < RoleSerializer - cache key: 'inherited_role', only: [:name, :special_attribute] - attribute :special_attribute + belongs_to :writer end class Comment < ::Model - attributes :body + attributes :id, :body associations :post, :author # Uses a custom non-time-based cache key @@ -58,14 +74,52 @@ def cache_key "comment/#{id}" end end + class CommentSerializer < ActiveModel::Serializer + cache expires_in: 1.day, skip_digest: true + attributes :id, :body + belongs_to :post + belongs_to :author + end + + class Post < ::Model + attributes :id, :title, :body + associations :author, :comments, :blog + end + class PostSerializer < ActiveModel::Serializer + cache key: 'post', expires_in: 0.1, skip_digest: true + attributes :id, :title, :body + + has_many :comments + belongs_to :blog + belongs_to :author + end + + class Role < ::Model + attributes :name, :description, :special_attribute + associations :author + end + class RoleSerializer < ActiveModel::Serializer + cache only: [:name, :slug], skip_digest: true + attributes :id, :name, :description + attribute :friendly_id, key: :slug + belongs_to :author + + def friendly_id + "#{object.name}-#{object.id}" + end + end + class InheritedRoleSerializer < RoleSerializer + cache key: 'inherited_role', only: [:name, :special_attribute] + attribute :special_attribute + end setup do cache_store.clear @comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') - @post = Post.new(title: 'New Post', body: 'Body') + @post = Post.new(id: 'post', title: 'New Post', body: 'Body') @bio = Bio.new(id: 1, content: 'AMS Contributor') - @author = Author.new(name: 'Joao M. D. Moura') - @blog = Blog.new(id: 999, name: 'Custom blog', writer: @author, articles: []) + @author = Author.new(id: 'author', name: 'Joao M. D. Moura') + @blog = Blog.new(id: 999, name: 'Custom blog', writer: @author) @role = Role.new(name: 'Great Author') @location = Location.new(lat: '-23.550520', lng: '-46.633309') @place = Place.new(name: 'Amazing Place') @@ -325,12 +379,14 @@ def test_a_serializer_rendered_by_two_adapter_returns_differently_fetch_attribut def test_uses_file_digest_in_cache_key render_object_with_cache(@blog) - key = "#{@blog.cache_key}/#{adapter.cache_key}/#{::Model::FILE_DIGEST}" + file_digest = Digest::MD5.hexdigest(File.open(__FILE__).read) + key = "#{@blog.cache_key}/#{adapter.cache_key}/#{file_digest}" assert_equal(@blog_serializer.attributes, cache_store.fetch(key)) end def test_cache_digest_definition - assert_equal(::Model::FILE_DIGEST, @post_serializer.class._cache_digest) + file_digest = Digest::MD5.hexdigest(File.open(__FILE__).read) + assert_equal(file_digest, @post_serializer.class._cache_digest) end def test_object_cache_keys diff --git a/test/fixtures/poro.rb b/test/fixtures/poro.rb index 3c804ccca..6245ad23d 100644 --- a/test/fixtures/poro.rb +++ b/test/fixtures/poro.rb @@ -1,10 +1,7 @@ class Model < ActiveModelSerializers::Model - FILE_DIGEST = Digest::MD5.hexdigest(File.open(__FILE__).read) + rand(2).zero? && derive_attributes_from_names_and_fix_accessors - # Defaults to the downcased model name. - def id - @id ||= self.class.model_name.name.downcase - end + attr_writer :id # At this time, just for organization of intent class_attribute :association_names @@ -23,6 +20,10 @@ def associations result[association_name] = public_send(association_name).freeze end.with_indifferent_access.freeze end + + def attributes + super.except(*association_names) + end end # see @@ -107,10 +108,6 @@ class PostPreviewSerializer < ActiveModel::Serializer has_many :comments, serializer: ::CommentPreviewSerializer belongs_to :author, serializer: ::AuthorPreviewSerializer end -class PostWithTagsSerializer < ActiveModel::Serializer - attributes :id - has_many :tags -end class PostWithCustomKeysSerializer < ActiveModel::Serializer attributes :id has_many :comments, key: :reviews @@ -207,10 +204,6 @@ class UnrelatedLinkSerializer < ActiveModel::Serializer end end -class Tag < Model - attributes :name -end - class VirtualValue < Model; end class VirtualValueSerializer < ActiveModel::Serializer attributes :id diff --git a/test/serializers/associations_test.rb b/test/serializers/associations_test.rb index 6d6447c35..90d213dca 100644 --- a/test/serializers/associations_test.rb +++ b/test/serializers/associations_test.rb @@ -2,13 +2,17 @@ module ActiveModel class Serializer class AssociationsTest < ActiveSupport::TestCase + class ModelWithoutSerializer < ::Model + attributes :id, :name + end + def setup @author = Author.new(name: 'Steve K.') @author.bio = nil @author.roles = [] @blog = Blog.new(name: 'AMS Blog') @post = Post.new(title: 'New Post', body: 'Body') - @tag = Tag.new(id: 'tagid', name: '#hashtagged') + @tag = ModelWithoutSerializer.new(id: 'tagid', name: '#hashtagged') @comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') @post.comments = [@comment] @post.tags = [@tag] @@ -46,7 +50,11 @@ def test_has_many_and_has_one end def test_has_many_with_no_serializer - PostWithTagsSerializer.new(@post).associations.each do |association| + post_serializer_class = Class.new(ActiveModel::Serializer) do + attributes :id + has_many :tags + end + post_serializer_class.new(@post).associations.each do |association| key = association.key serializer = association.serializer options = association.options