Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Model#attributes helper; make test attributes explicit #2021

Merged
merged 1 commit into from
Jan 8, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@ Breaking changes:

Features:

- [#2021](https://github.com/rails-api/active_model_serializers/pull/2021) ActiveModelSerializers::Model#attributes. (@bf4)

Fixes:

Misc:

- [#2021](https://github.com/rails-api/active_model_serializers/pull/2021) Make test attributes explicit. Tests have Model#associations. (@bf4)

### [v0.10.4 (2017-01-06)](https://github.com/rails-api/active_model_serializers/compare/v0.10.3...v0.10.4)

Misc:
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ class SomeResource < ActiveRecord::Base
end
# or
class SomeResource < ActiveModelSerializers::Model
attr_accessor :title, :body
attributes :title, :body
end
```

Expand Down Expand Up @@ -279,7 +279,7 @@ which is a simple serializable PORO (Plain-Old Ruby Object).

```ruby
class MyModel < ActiveModelSerializers::Model
attr_accessor :id, :name, :level
attributes :id, :name, :level
end
```

Expand Down
23 changes: 18 additions & 5 deletions docs/howto/serialize_poro.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

related #1910


```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]
Expand All @@ -21,12 +24,22 @@ 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
attr_accessor :id, :name, :level
attributes :id, :name, :level
end
```

The default serializer would be `MyModelSerializer`.
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).
8 changes: 8 additions & 0 deletions lib/active_model_serializers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ def self.default_include_directive
@default_include_directive ||= JSONAPI::IncludeDirective.new(config.default_includes, allow_wildcard: true)
end

def self.silence_warnings
original_verbose = $VERBOSE
$VERBOSE = nil
yield
ensure
$VERBOSE = original_verbose
end

require 'active_model/serializer/version'
require 'active_model/serializer'
require 'active_model/serializable_resource'
Expand Down
92 changes: 72 additions & 20 deletions lib/active_model_serializers/model.rb
Original file line number Diff line number Diff line change
@@ -1,40 +1,92 @@
# 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::Model
include ActiveModel::Serializers::JSON
include ActiveModel::Model

# 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<String, Symbol>]
# @param name [String, Symbol]
def self.attributes(*names)
# Silence redefinition of methods warnings
ActiveModelSerializers.silence_warnings do
attr_accessor(*names)
end
end

# Support for validation and other ActiveModel::Errors
# @return [ActiveModel::Errors]
attr_reader :errors

# (see #updated_at)
attr_writer :updated_at

attr_reader :attributes, :errors
# 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 = attributes && attributes.symbolize_keys
attributes ||= {} # protect against nil
@attributes = attributes.symbolize_keys.with_indifferent_access
@errors = ActiveModel::Errors.new(self)
super
end

# 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 <tt>attributes[:id] = 5</tt>.
# @return [String, Numeric, Symbol]
def id
attributes.fetch(:id) { self.class.name.downcase }
end

# Defaults to the downcased model name and updated_at
def cache_key
attributes.fetch(:cache_key) { "#{self.class.name.downcase}/#{id}-#{updated_at.strftime('%Y%m%d%H%M%S%9N')}" }
attributes.fetch(:id) do
defined?(@id) ? @id : self.class.model_name.name && self.class.model_name.name.downcase
end
end

# Defaults to the time the serializer file was modified.
# 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 <tt>attributes[:updated_at] = Time.current</tt>.
# @return [String, Numeric, Time]
def updated_at
attributes.fetch(:updated_at) { File.mtime(__FILE__) }
attributes.fetch(:updated_at) do
defined?(@updated_at) ? @updated_at : File.mtime(__FILE__)
end
end

def read_attribute_for_serialization(key)
if key == :id || key == 'id'
attributes.fetch(key) { id }
else
attributes[key]
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,
"#{id}-#{updated_at.strftime('%Y%m%d%H%M%S%9N')}"
].compact)
end

# The following methods are needed to be minimally implemented for ActiveModel::Errors
Expand Down
4 changes: 2 additions & 2 deletions test/action_controller/adapter_selector_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ def render_using_adapter_override
end

def render_skipping_adapter
@profile = Profile.new(name: 'Name 1', description: 'Description 1', comments: 'Comments 1')
@profile = Profile.new(id: 'render_skipping_adapter_id', name: 'Name 1', description: 'Description 1', comments: 'Comments 1')
render json: @profile, adapter: false
end
end
Expand Down Expand Up @@ -46,7 +46,7 @@ def test_render_using_adapter_override

def test_render_skipping_adapter
get :render_skipping_adapter
assert_equal '{"name":"Name 1","description":"Description 1","comments":"Comments 1"}', response.body
assert_equal '{"id":"render_skipping_adapter_id","name":"Name 1","description":"Description 1"}', response.body
end
end
end
Expand Down
21 changes: 15 additions & 6 deletions test/action_controller/json_api/fields_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,16 @@ module Serialization
class JsonApi
class FieldsTest < ActionController::TestCase
class FieldsTestController < ActionController::Base
class PostSerializer < ActiveModel::Serializer
class AuthorWithName < Author
attributes :first_name, :last_name
end
class AuthorWithNameSerializer < AuthorSerializer
type 'authors'
end
class PostWithPublishAt < Post
attributes :publish_at
end
class PostWithPublishAtSerializer < ActiveModel::Serializer
type 'posts'
attributes :title, :body, :publish_at
belongs_to :author
Expand All @@ -14,19 +23,19 @@ class PostSerializer < ActiveModel::Serializer

def setup_post
ActionController::Base.cache_store.clear
@author = Author.new(id: 1, first_name: 'Bob', last_name: 'Jones')
@author = AuthorWithName.new(id: 1, first_name: 'Bob', last_name: 'Jones')
@comment1 = Comment.new(id: 7, body: 'cool', author: @author)
@comment2 = Comment.new(id: 12, body: 'awesome', author: @author)
@post = Post.new(id: 1337, title: 'Title 1', body: 'Body 1',
author: @author, comments: [@comment1, @comment2],
publish_at: '2020-03-16T03:55:25.291Z')
@post = PostWithPublishAt.new(id: 1337, title: 'Title 1', body: 'Body 1',
author: @author, comments: [@comment1, @comment2],
publish_at: '2020-03-16T03:55:25.291Z')
@comment1.post = @post
@comment2.post = @post
end

def render_fields_works_on_relationships
setup_post
render json: @post, serializer: PostSerializer, adapter: :json_api, fields: { posts: [:author] }
render json: @post, serializer: PostWithPublishAtSerializer, adapter: :json_api, fields: { posts: [:author] }
end
end

Expand Down
14 changes: 11 additions & 3 deletions test/action_controller/json_api/transform_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,17 @@ module Serialization
class JsonApi
class KeyTransformTest < ActionController::TestCase
class KeyTransformTestController < ActionController::Base
class Post < ::Model; end
class Author < ::Model; end
class TopComment < ::Model; end
class Post < ::Model
attributes :title, :body, :publish_at
associations :author, :top_comments
end
class Author < ::Model
attributes :first_name, :last_name
end
class TopComment < ::Model
attributes :body
associations :author, :post
end
class PostSerializer < ActiveModel::Serializer
type 'posts'
attributes :title, :body, :publish_at
Expand Down
18 changes: 12 additions & 6 deletions test/action_controller/namespace_lookup_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,16 @@
module ActionController
module Serialization
class NamespaceLookupTest < ActionController::TestCase
class Book < ::Model; end
class Page < ::Model; end
class Chapter < ::Model; end
class Writer < ::Model; end
class Book < ::Model
attributes :title, :body
associations :writer, :chapters
end
class Chapter < ::Model
attributes :title
end
class Writer < ::Model
attributes :name
end

module Api
module V2
Expand Down Expand Up @@ -93,7 +99,7 @@ def explicit_namespace_as_symbol
end

def invalid_namespace
book = Book.new(title: 'New Post', body: 'Body')
book = Book.new(id: 'invalid_namespace_book_id', title: 'New Post', body: 'Body')

render json: book, namespace: :api_v2
end
Expand Down Expand Up @@ -205,7 +211,7 @@ def namespace_set_by_request_headers

assert_serializer ActiveModel::Serializer::Null

expected = { 'title' => 'New Post', 'body' => 'Body' }
expected = { 'id' => 'invalid_namespace_book_id', 'title' => 'New Post', 'body' => 'Body' }
actual = JSON.parse(@response.body)

assert_equal expected, actual
Expand Down
6 changes: 3 additions & 3 deletions test/action_controller/serialization_scope_name_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,16 @@

module SerializationScopeTesting
class User < ActiveModelSerializers::Model
attr_accessor :id, :name, :admin
attributes :id, :name, :admin
def admin?
admin
end
end
class Comment < ActiveModelSerializers::Model
attr_accessor :id, :body
attributes :id, :body
end
class Post < ActiveModelSerializers::Model
attr_accessor :id, :title, :body, :comments
attributes :id, :title, :body, :comments
end
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body, :comments
Expand Down
2 changes: 1 addition & 1 deletion test/action_controller/serialization_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def render_fragment_changed_object_with_relationship
like = Like.new(id: 1, likeable: comment, time: 3.days.ago)

generate_cached_serializer(like)
like.likable = comment2
like.likeable = comment2
like.time = Time.zone.now.to_s

render json: like
Expand Down
Loading