-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Add support for nested serializers. #1157
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
Changes from all commits
69b26d2
8dfef17
6bcb84b
4b50f01
dd580cb
b3c12ec
61035bc
3f00e79
273f0a7
ef6577d
508ba34
597ae65
3ef1149
71816a5
db24432
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
module ActiveModel::Serializer::Utils | ||
module_function | ||
|
||
# Gives the existing lookup paths that are suffixes of the fully qualified path of `klass`. | ||
# @param [Object] klass | ||
# @return [Array<Symbol>] | ||
def nested_lookup_paths(klass) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. can you add some yardoc? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done 👍 |
||
paths = klass.to_s.split('::').reverse.reduce([]) { |a, e| a + [[e] + Array(a.last)] }.reverse | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would you mind helping me read this code?
Is that right? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is indeed. I admit that part was slightly cryptic. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So that as the test says
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yessir. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I believe so. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So that @share1 = Share.new
TweetSerializer.serializer_for(@share1) #=> TweetSerializer::ShareSerializer
ActiveModel::Serializer.serializer_for(@share1) #=> ShareSerializer There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exactly. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So that you can build associations on tweet.author using There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, exactly, if available, and safely fallback to any less specialized serializer that can handle an |
||
paths.map! { |path| "#{path.join('::')}" } | ||
paths.map! do |path| | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'll comment the first line, which might be pretty cryptic. The rest is pretty much self-explanatory, no? |
||
begin | ||
path.constantize | ||
rescue NameError | ||
nil | ||
end | ||
end | ||
paths.delete(nil) | ||
paths.push(Object) | ||
|
||
paths | ||
end | ||
|
||
# Gives the class named `class_name` if it exists (directly) inside one of the lookup `paths`. | ||
# @param [Array<Symbol>] paths | ||
# @param [Symbol, String] class_name | ||
# @return [Object, nil] | ||
def nested_lookup(paths, class_name) | ||
class_path = paths.find { |path| path.const_defined?(class_name, false) } | ||
class_path.const_get(class_name) if class_path | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,35 @@ | ||
require 'test_helper' | ||
|
||
module ActiveModel | ||
class Serializer | ||
module Adapter | ||
class JsonApi | ||
class NestedSerializersTest < Minitest::Test | ||
def setup | ||
@tweet = Tweet.new(id: 1, body: 'Tweet 1', date: 'Jan 15') | ||
@share1 = Share.new(id: 1, platform: 'facebook', date: 'Jan 16') | ||
@author = Author.new(id: 1, name: 'Lucas H.') | ||
@tweet.author = @author | ||
@tweet.shares = [@share1] | ||
@share1.author = @author | ||
@author.posts = [] | ||
@author.roles = [] | ||
@author.bio = nil | ||
end | ||
|
||
def test_nested_serializers | ||
actual = ActiveModel::SerializableResource.new(@tweet, adapter: :json_api).serializable_hash | ||
expected = { | ||
data: { | ||
id: '1', type: 'tweets', attributes: { body: 'Tweet 1', date: 'Jan 15' }, | ||
relationships: { author: { data: { id: '1', type: 'authors' } }, | ||
shares: { data: [{ id: '1', type: 'shares' }] } } | ||
} | ||
} | ||
assert_equal(expected, actual) | ||
end | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -79,6 +79,8 @@ class ProfilePreviewSerializer < ActiveModel::Serializer | |
Place = Class.new(Model) | ||
Tag = Class.new(Model) | ||
VirtualValue = Class.new(Model) | ||
Tweet = Class.new(Model) | ||
Share = Class.new(Model) | ||
Comment = Class.new(Model) do | ||
# Uses a custom non-time-based cache key | ||
def cache_key | ||
|
@@ -258,4 +260,34 @@ def maker | |
cache only: [:id] | ||
attributes :id | ||
end | ||
|
||
class TweetSerializer < ActiveModel::Serializer | ||
attributes :id, :body, :date | ||
|
||
belongs_to :author | ||
class AuthorSerializer < ActiveModel::Serializer | ||
attributes :id | ||
end | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. How is this different from There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One thing is that the namespacing makes it so that you don't have to use funky adapter names that you then have to specify manually. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So, this PR is more a syntactic sugar than a new feature? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. that's sorta how I see it as well. But for me, the biggest benefit would be to greatly reduce the number of top level serializers that actually have VERY specific purposes There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is, indeed syntactic sugar. However, I could see in the future having, say the Json adapter default to including all nested relationships (default to |
||
|
||
has_many :shares | ||
class ShareSerializer < ActiveModel::Serializer | ||
attributes :id, :platform | ||
|
||
belongs_to :author | ||
class AuthorSerializer < ActiveModel::Serializer | ||
attributes :id, :name | ||
end | ||
end | ||
end | ||
|
||
class ShareSerializer < ActiveModel::Serializer | ||
attributes :id, :platform, :date | ||
|
||
belongs_to :author | ||
class AuthorSerializer < ActiveModel::Serializer | ||
attributes :id, :name, :email | ||
end | ||
belongs_to :post | ||
end | ||
|
||
$VERBOSE = verbose |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,24 @@ | ||
require 'test_helper' | ||
|
||
module ActiveModel | ||
class Serializer | ||
module Utils | ||
class NestedLookupPathsTest < Minitest::Test | ||
def test_activemodel_serializer | ||
expected = [::ActiveModel::Serializer, ::Object] | ||
actual = Utils.nested_lookup_paths(ActiveModel::Serializer) | ||
assert_equal(expected, actual) | ||
end | ||
|
||
def test_nested_classes | ||
expected = [::TweetSerializer::ShareSerializer::AuthorSerializer, | ||
::ShareSerializer::AuthorSerializer, | ||
::AuthorSerializer, | ||
::Object] | ||
actual = Utils.nested_lookup_paths(TweetSerializer::ShareSerializer::AuthorSerializer) | ||
assert_equal(expected, actual) | ||
end | ||
end | ||
end | ||
end | ||
end |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
require 'test_helper' | ||
|
||
module ActiveModel | ||
class Serializer | ||
module Utils | ||
class NestedLookupTest < Minitest::Test | ||
def test_existing | ||
paths = [::TweetSerializer, ::Object] | ||
expected = ::TweetSerializer::ShareSerializer | ||
actual = Utils.nested_lookup(paths, 'ShareSerializer') | ||
assert_equal(expected, actual) | ||
end | ||
|
||
def test_non_existing | ||
paths = [::Object] | ||
actual = Utils.nested_lookup(paths, 'NonExistingSerializer') | ||
assert_nil(actual) | ||
end | ||
end | ||
end | ||
end | ||
end |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is subject called parent?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because, as any parent, it is its own subject, but its children's parent. The
serializer_options
is the hash that we build for passing down to the associated (i.e. child) serializer.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you feel strongly about this, and think of a better name, I don't mind changing it.