Skip to content
Closed
Show file tree
Hide file tree
Changes from 9 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
36 changes: 24 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])
end
end

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

def self.get_serializer_for(klass)
def self.get_serializer_for(klass, parent_serializer)
Copy link
Contributor

Choose a reason for hiding this comment

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

can you add some yardoc to various parts of this method? explaining what each section does, and what scenarios will be applicable to each block of code?

serializers_cache.fetch_or_store(klass) do
serializer_class_name = "#{klass.name}Serializer"
serializer_class = serializer_class_name.safe_constantize

nested_serializer_class_name = "#{self}::#{serializer_class_name}"
serializer_class = nested_serializer_class_name.safe_constantize

if parent_serializer
namespaced_serializer_class_name = "#{parent_serializer.root_serializer.class.name.deconstantize}::#{serializer_class_name}"
serializer_class ||= namespaced_serializer_class_name.safe_constantize
end

serializer_class ||= serializer_class_name.safe_constantize

if serializer_class
serializer_class
elsif klass.superclass
get_serializer_for(klass.superclass)
get_serializer_for(klass.superclass, parent_serializer)
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 +141,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 +164,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