Skip to content
Closed
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: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ Features:
- [#1172](https://github.com/rails-api/active_model_serializers/pull/1172) Better serializer registration, get more than just the first module (@bf4)
- [#1158](https://github.com/rails-api/active_model_serializers/pull/1158) Add support for wildcards in `include` option (@beauby)
- [#1127](https://github.com/rails-api/active_model_serializers/pull/1127) Add support for nested
associations for JSON and Attributes adapters via the `include` option (@NullVoxPopuli, @beauby).
associations for JSON and Attributes adapters via the `include` option (@NullVoxPopuli, @beauby)
- [#1193](https://github.com/rails-api/active_model_serializers/pull/1193) Add support for inline nested serializers (@beauby)
Copy link
Member

Choose a reason for hiding this comment

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

1193?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This PR is based on #1193, that's why.

- [#1196](https://github.com/rails-api/active_model_serializers/pull/1196) Add support for namespaced serializers (@beauby)

Fixes:

Expand Down
3 changes: 2 additions & 1 deletion lib/action_controller/serialization.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ def get_serializer(resource, options = {})
"Please pass 'adapter: false' or see ActiveSupport::SerializableResource.new"
options[:adapter] = false
end
serializable_resource = ActiveModel::SerializableResource.new(resource, options)
serializable_options = options.merge(controller: self)
Copy link
Member

Choose a reason for hiding this comment

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

why not mutate?

Copy link
Member

Choose a reason for hiding this comment

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

also why change options when not serializing

Copy link
Member

Choose a reason for hiding this comment

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

and do we need the whole controller? I'd rather not and looks like you just want the class name

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Currently we just need the class name but there's virtually 0 overhead to getting the instance, so why not?

Copy link
Member

Choose a reason for hiding this comment

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

If you make it easy to use the controller directly, people will, and the scope will creep and and new ams will gain responsibilities it shouldn't gave and cause pain and bugs to users and maintainers

Failure of Interface segregation, not keepig it narrow

Copy link
Contributor Author

Choose a reason for hiding this comment

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

In my opinion, the real issue lies in the fact that we miss a layer of separation between the serializer definition and the serializer instances.

serializable_resource = ActiveModel::SerializableResource.new(resource, serializable_options)
if serializable_resource.serializer?
serializable_resource.serialization_scope ||= serialization_scope
serializable_resource.serialization_scope_name = _serialization_scope
Expand Down
57 changes: 45 additions & 12 deletions lib/active_model/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,10 @@ def self.serializer_for(resource, options = {})
resource.serializer_class
elsif resource.respond_to?(:to_ary)
config.array_serializer
elsif options.key?(:serializer)
options[:serializer]
else
options.fetch(:serializer) { get_serializer_for(resource.class) }
get_serializer_for(resource.class, options[:parent_serializer], options[:controller])
Copy link
Member

Choose a reason for hiding this comment

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

this is getting pretty hairy

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I think the whole serializer_for calling get_serializer_for thing is pretty bad design already. I'm really open to suggestions to make the whole serializer lookup cleaner. (I initially made this PR in a "less code change possible" mindset so that it would be easier to review.)

end
end

Expand All @@ -108,20 +110,50 @@ def self.digest_caller_file(caller_line)
Digest::MD5.hexdigest(serializer_file_contents)
end

def self.get_serializer_for(klass)
# Compute the lookup chain for a given serializer class, parent serializer and controller
# @param [String] serializer_class_name The class name to lookup
# @param [ActiveModel::Serializer] parent_serializer The instance of the parent serializer, if any
# @param [ActionController] controller The instance of the calling controller, if any
#
# @return [Array<String>] The lookup chain
#
def self.serializer_lookup_chain_for(serializer_class_name, parent_serializer, controller)
chain = []

# Look for a serializer nested inside the current serializer first, if inside a user-defined serializer
chain.push("#{self}::#{serializer_class_name}") if self.class != ActiveModel::Serializer
Copy link
Contributor Author

Choose a reason for hiding this comment

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

That kind of check makes me believe it might be time to make serializer_for an instance method?

Copy link
Member

Choose a reason for hiding this comment

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

It's been around for a while, so I'd rather deprecate it


# Look for a serializer inside the controller namespace
chain.push("#{controller.class.name.deconstantize}::#{serializer_class_name}") if controller

# Look for a serializer inside the root namespace (i.e. that of the first serializer of the chain)
if parent_serializer
chain.push("#{parent_serializer.root_serializer.class.name.deconstantize}::#{serializer_class_name}")
elsif self.class != ActiveModel::Serializer # The first serializer of the chain does not have a parent
chain.push("#{name.deconstantize}::#{serializer_class_name}")
end

# Finally, look for the serializer in the global namespace
chain.push(serializer_class_name)

chain
end

def self.get_serializer_for(klass, parent_serializer, controller)
serializers_cache.fetch_or_store(klass) do
serializer_class_name = "#{klass.name}Serializer"
serializer_class = serializer_class_name.safe_constantize
serializer_class = serializer_lookup_chain_for(serializer_class_name, parent_serializer, controller).lazy
.map(&:safe_constantize).find { |x| x }

if serializer_class
serializer_class
elsif klass.superclass
get_serializer_for(klass.superclass)
get_serializer_for(klass.superclass, parent_serializer, controller)
end
end
end

attr_accessor :object, :root, :meta, :meta_key, :scope
attr_accessor :object, :root, :meta, :meta_key, :scope, :parent_serializer, :root_serializer

def initialize(object, options = {})
self.object = object
Expand All @@ -130,12 +162,13 @@ def initialize(object, options = {})
self.meta = instance_options[:meta]
self.meta_key = instance_options[:meta_key]
self.scope = instance_options[:scope]
self.parent_serializer = instance_options[:parent_serializer]
self.root_serializer = (parent_serializer && parent_serializer.root_serializer) || self

scope_name = instance_options[:scope_name]
if scope_name && !respond_to?(scope_name)
self.class.class_eval do
define_method scope_name, lambda { scope }
end
return unless scope_name && !respond_to?(scope_name)
self.class.class_eval do
define_method scope_name, -> { scope }
end
end

Expand All @@ -152,10 +185,10 @@ def attributes(options = {})
end

attributes.each_with_object({}) do |name, hash|
unless self.class._fragmented
hash[name] = send(name)
else
if self.class._fragmented
hash[name] = self.class._fragmented.public_send(name)
else
hash[name] = send(name)
end
end
end
Expand Down
13 changes: 8 additions & 5 deletions lib/active_model/serializer/array_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,23 @@ class ArraySerializer
include Enumerable
delegate :each, to: :@serializers

attr_reader :object, :root, :meta, :meta_key
attr_reader :object, :root, :meta, :meta_key, :parent_serializer, :root_serializer

def initialize(resources, options = {})
@root = options[:root]
@object = resources
@parent_serializer = options[:parent_serializer]
@root_serializer = @parent_serializer.try(:root_serializer)
lookup_serializer = (@parent_serializer && @parent_serializer.class) || ActiveModel::Serializer
@serializers = resources.map do |resource|
serializer_class = options.fetch(:serializer) do
ActiveModel::Serializer.serializer_for(resource)
end
serializer_class = options.fetch(:serializer) { lookup_serializer.serializer_for(resource) }

if serializer_class.nil?
fail NoSerializerError, "No serializer found for resource: #{resource.inspect}"
else
serializer_class.new(resource, options.except(:serializer))
serializer_options = options.except(:serializer)
serializer_options.merge!(parent_serializer: @parent_serializer) if @parent_serializer
serializer_class.new(resource, serializer_options)
end
end
@meta = options[:meta]
Expand Down
36 changes: 26 additions & 10 deletions lib/active_model/serializer/associations.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# 2420012eliyel - popincool - 22 cite popincourt - 4e premiere droite
module ActiveModel
class Serializer
# Defines an association in the object should be rendered.
Expand Down Expand Up @@ -29,7 +30,7 @@ class << base

module ClassMethods
def inherited(base)
base._reflections = self._reflections.try(:dup) || []
base._reflections = _reflections.try(:dup) || []
end

# @param [Symbol] name of the association
Expand All @@ -39,8 +40,8 @@ def inherited(base)
# @example
# has_many :comments, serializer: CommentSummarySerializer
#
def has_many(name, options = {})
associate HasManyReflection.new(name, options)
def has_many(name, options = {}, &block)
associate(HasManyReflection.new(name, options), &block)
end

# @param [Symbol] name of the association
Expand All @@ -50,8 +51,8 @@ def has_many(name, options = {})
# @example
# belongs_to :author, serializer: AuthorSerializer
#
def belongs_to(name, options = {})
associate BelongsToReflection.new(name, options)
def belongs_to(name, options = {}, &block)
associate(BelongsToReflection.new(name, options), &block)
end

# @param [Symbol] name of the association
Expand All @@ -61,26 +62,41 @@ def belongs_to(name, options = {})
# @example
# has_one :author, serializer: AuthorSerializer
#
def has_one(name, options = {})
associate HasOneReflection.new(name, options)
def has_one(name, options = {}, &block)
associate(HasOneReflection.new(name, options), &block)
end

private

# Add reflection and define {name} accessor.
# Add reflection and define {name} accessor and nested serializer.
# @param [ActiveModel::Serializer::Reflection] reflection
# @return [void]
#
# @api private
#
def associate(reflection)
def associate(reflection, &block)
self._reflections = _reflections.dup

define_method reflection.name do
object.send reflection.name
end unless method_defined?(reflection.name)

self._reflections << reflection
_reflections << reflection

define_nested_serializer(reflection.name.to_s.singularize, &block) if block_given?
Copy link
Member

Choose a reason for hiding this comment

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

if the block is optional it shouldn't be a param. maybe this (chain of) method is doing too much?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Why should it not be a param if it's optional? I'm open to suggestions for doing this differently.

Copy link
Member

Choose a reason for hiding this comment

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

usually you'd only use &block if you need an object, otherwise, you'd just

def foo
  if block_given?
   puts yield
  else
   puts 'no block'
  end
end

Copy link
Contributor Author

Choose a reason for hiding this comment

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

&block needs to be a param (i.e. named block) if you want to pass it as an argument to a method call. method(yield) does not work, nor does method do yield end (the self object gets messed up).

Copy link
Member

Choose a reason for hiding this comment

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

I get that, i just mean design wise.. Also could check if block since you have the object

B mobile phone

On Oct 2, 2015, at 8:13 AM, Lucas Hosseini [email protected] wrote:

In lib/active_model/serializer/associations.rb:

       self._reflections = _reflections.dup

       define_method reflection.name do
         object.send reflection.name
       end unless method_defined?(reflection.name)
  •      self._reflections << reflection
    
  •      _reflections << reflection
    
  •      define_nested_serializer(reflection.name.to_s.singularize, &block) if block_given?
    
    &block needs to be a param (i.e. named block) if you want to pass it as an argument to a method call. method(yield) does not work, nor does method do yield end (the self object gets messed up).


Reply to this email directly or view it on GitHub.

end

# Define a nested serializer
# @param [String] resource_name The name of the association
# @return [void]
#
# @api private
#
def define_nested_serializer(resource_name, &block)
serializer_name = "#{resource_name.camelize}Serializer"
serializer = Class.new(ActiveModel::Serializer)
Copy link
Member

Choose a reason for hiding this comment

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

frankly, all this meta programming scares me. Is it really necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

What other solution do you have in mind?

serializer.class_eval(&block)
const_set(serializer_name, serializer)
end
end

Expand Down
7 changes: 4 additions & 3 deletions lib/active_model/serializer/reflection.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,13 @@ class Serializer
def build_association(subject, parent_serializer_options)
association_value = subject.send(name)
reflection_options = options.dup
serializer_class = ActiveModel::Serializer.serializer_for(association_value, reflection_options)
serializer_class = subject.class.serializer_for(association_value, reflection_options.merge(parent_serializer: subject))

if serializer_class
begin
serializer = serializer_class.new(
association_value,
serializer_options(parent_serializer_options, reflection_options)
serializer_options(subject, parent_serializer_options, reflection_options)
)
rescue ActiveModel::Serializer::ArraySerializer::NoSerializerError
reflection_options[:virtual_value] = association_value.try(:as_json) || association_value
Expand All @@ -62,11 +62,12 @@ def build_association(subject, parent_serializer_options)

private

def serializer_options(parent_serializer_options, reflection_options)
def serializer_options(subject, parent_serializer_options, reflection_options)
serializer = reflection_options.fetch(:serializer, nil)

serializer_options = parent_serializer_options.except(:serializer)
serializer_options[:serializer] = serializer if serializer
serializer_options[:parent_serializer] = subject
serializer_options
end
end
Expand Down
29 changes: 29 additions & 0 deletions test/adapter/json/nested_serializers_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
require 'test_helper'

module ActiveModel
class Serializer
module Adapter
class Json
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.')
Copy link
Member

Choose a reason for hiding this comment

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

Please define all of these here. I can't tell what you're testing.. doesn't look nested, y'know?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

True, that's why I opened #1206. I really think serializers/resources should be defined in each test file somehow.

@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).serializable_hash
expected = { tweet: { id: 1, body: 'Tweet 1', date: 'Jan 15', author: { id: 1 }, shares: [{ id: 1, platform: 'facebook' }] } }
assert_equal(expected, actual)
end
end
end
end
end
end
28 changes: 28 additions & 0 deletions test/fixtures/poro.rb
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ def cache_key
"#{self.class.name.downcase}/#{self.id}"
end
end
Tweet = Class.new(Model)
Share = Class.new(Model)

module Spam; end
Spam::UnrelatedLink = Class.new(Model)
Expand Down Expand Up @@ -258,4 +260,30 @@ def maker
cache only: [:id]
attributes :id
end

class TweetSerializer < ActiveModel::Serializer
attributes :id, :body, :date

belongs_to :author do
attributes :id
end

has_many :shares do
attributes :id, :platform

belongs_to :author do
attributes :id, :name
end
end
end

class ShareSerializer < ActiveModel::Serializer
attributes :id, :platform, :date

belongs_to :author do
attributes :id, :name, :email
end
belongs_to :post
end

$VERBOSE = verbose
7 changes: 7 additions & 0 deletions test/serializers/serializer_for_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ def setup
@my_profile = MyProfile.new
@custom_profile = CustomProfile.new
@model = ::Model.new
@tweet = Tweet.new
@share1 = Share.new
end

def test_serializer_for_existing_serializer
Expand All @@ -59,6 +61,11 @@ def test_serializer_custom_serializer
serializer = ActiveModel::Serializer.serializer_for(@custom_profile)
assert_equal ProfileSerializer, serializer
end

def test_serializer_for_nested_resource
serializer = TweetSerializer.serializer_for(@share1)
assert_equal(TweetSerializer::ShareSerializer, serializer)
end
end
end
end
Expand Down