mirror of
https://github.com/ditkrg/active_model_serializers.git
synced 2026-01-23 06:16:50 +00:00
Merge pull request #1356 from bf4/attribute_objects
Add inline syntax for attributes and associations
This commit is contained in:
commit
f562449bd4
@ -3,6 +3,7 @@ require 'active_model/serializer/collection_serializer'
|
|||||||
require 'active_model/serializer/array_serializer'
|
require 'active_model/serializer/array_serializer'
|
||||||
require 'active_model/serializer/include_tree'
|
require 'active_model/serializer/include_tree'
|
||||||
require 'active_model/serializer/associations'
|
require 'active_model/serializer/associations'
|
||||||
|
require 'active_model/serializer/attributes'
|
||||||
require 'active_model/serializer/configuration'
|
require 'active_model/serializer/configuration'
|
||||||
require 'active_model/serializer/fieldset'
|
require 'active_model/serializer/fieldset'
|
||||||
require 'active_model/serializer/lint'
|
require 'active_model/serializer/lint'
|
||||||
@ -13,6 +14,7 @@ module ActiveModel
|
|||||||
class Serializer
|
class Serializer
|
||||||
include Configuration
|
include Configuration
|
||||||
include Associations
|
include Associations
|
||||||
|
include Attributes
|
||||||
require 'active_model/serializer/adapter'
|
require 'active_model/serializer/adapter'
|
||||||
|
|
||||||
# Matches
|
# Matches
|
||||||
@ -45,14 +47,9 @@ module ActiveModel
|
|||||||
end
|
end
|
||||||
|
|
||||||
with_options instance_writer: false, instance_reader: false do |serializer|
|
with_options instance_writer: false, instance_reader: false do |serializer|
|
||||||
class_attribute :_type, instance_reader: true
|
serializer.class_attribute :_type, instance_reader: true
|
||||||
class_attribute :_attributes # @api private : names of attribute methods, @see Serializer#attribute
|
serializer.class_attribute :_links # @api private : links definitions, @see Serializer#link
|
||||||
self._attributes ||= []
|
|
||||||
class_attribute :_attributes_keys # @api private : maps attribute value to explict key name, @see Serializer#attribute
|
|
||||||
self._attributes_keys ||= {}
|
|
||||||
class_attribute :_links # @api private : links definitions, @see Serializer#link
|
|
||||||
self._links ||= {}
|
self._links ||= {}
|
||||||
|
|
||||||
serializer.class_attribute :_cache # @api private : the cache object
|
serializer.class_attribute :_cache # @api private : the cache object
|
||||||
serializer.class_attribute :_fragmented # @api private : @see ::fragmented
|
serializer.class_attribute :_fragmented # @api private : @see ::fragmented
|
||||||
serializer.class_attribute :_cache_key # @api private : when present, is first item in cache_key
|
serializer.class_attribute :_cache_key # @api private : when present, is first item in cache_key
|
||||||
@ -69,12 +66,10 @@ module ActiveModel
|
|||||||
serializer.class_attribute :_cache_digest # @api private : Generated
|
serializer.class_attribute :_cache_digest # @api private : Generated
|
||||||
end
|
end
|
||||||
|
|
||||||
# Serializers inherit _attributes and _attributes_keys.
|
# Serializers inherit _attribute_mappings, _reflections, and _links.
|
||||||
# Generates a unique digest for each serializer at load.
|
# Generates a unique digest for each serializer at load.
|
||||||
def self.inherited(base)
|
def self.inherited(base)
|
||||||
caller_line = caller.first
|
caller_line = caller.first
|
||||||
base._attributes = _attributes.dup
|
|
||||||
base._attributes_keys = _attributes_keys.dup
|
|
||||||
base._links = _links.dup
|
base._links = _links.dup
|
||||||
base._cache_digest = digest_caller_file(caller_line)
|
base._cache_digest = digest_caller_file(caller_line)
|
||||||
super
|
super
|
||||||
@ -91,37 +86,6 @@ module ActiveModel
|
|||||||
_links[name] = block || value
|
_links[name] = block || value
|
||||||
end
|
end
|
||||||
|
|
||||||
# @example
|
|
||||||
# class AdminAuthorSerializer < ActiveModel::Serializer
|
|
||||||
# attributes :id, :name, :recent_edits
|
|
||||||
def self.attributes(*attrs)
|
|
||||||
attrs = attrs.first if attrs.first.class == Array
|
|
||||||
|
|
||||||
attrs.each do |attr|
|
|
||||||
attribute(attr)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# @example
|
|
||||||
# class AdminAuthorSerializer < ActiveModel::Serializer
|
|
||||||
# attributes :id, :recent_edits
|
|
||||||
# attribute :name, key: :title
|
|
||||||
#
|
|
||||||
# def recent_edits
|
|
||||||
# object.edits.last(5)
|
|
||||||
# enr
|
|
||||||
def self.attribute(attr, options = {})
|
|
||||||
key = options.fetch(:key, attr)
|
|
||||||
_attributes_keys[attr] = { key: key } if key != attr
|
|
||||||
_attributes << key unless _attributes.include?(key)
|
|
||||||
|
|
||||||
ActiveModelSerializers.silence_warnings do
|
|
||||||
define_method key do
|
|
||||||
object.read_attribute_for_serialization(attr)
|
|
||||||
end unless method_defined?(key) || _fragmented.respond_to?(attr)
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
# @api private
|
# @api private
|
||||||
# Used by FragmentCache on the CachedSerializer
|
# Used by FragmentCache on the CachedSerializer
|
||||||
# to call attribute methods on the fragmented cached serializer.
|
# to call attribute methods on the fragmented cached serializer.
|
||||||
@ -220,6 +184,15 @@ module ActiveModel
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def self._serializer_instance_method_defined?(name)
|
||||||
|
_serializer_instance_methods.include?(name)
|
||||||
|
end
|
||||||
|
|
||||||
|
def self._serializer_instance_methods
|
||||||
|
@_serializer_instance_methods ||= (public_instance_methods - Object.public_instance_methods).to_set
|
||||||
|
end
|
||||||
|
private_class_method :_serializer_instance_methods
|
||||||
|
|
||||||
attr_accessor :object, :root, :scope
|
attr_accessor :object, :root, :scope
|
||||||
|
|
||||||
# `scope_name` is set as :current_user by default in the controller.
|
# `scope_name` is set as :current_user by default in the controller.
|
||||||
@ -244,16 +217,13 @@ module ActiveModel
|
|||||||
root || object.class.model_name.to_s.underscore
|
root || object.class.model_name.to_s.underscore
|
||||||
end
|
end
|
||||||
|
|
||||||
# Return the +attributes+ of +object+ as presented
|
def read_attribute_for_serialization(attr)
|
||||||
# by the serializer.
|
if self.class._serializer_instance_method_defined?(attr)
|
||||||
def attributes(requested_attrs = nil)
|
send(attr)
|
||||||
self.class._attributes.each_with_object({}) do |name, hash|
|
elsif self.class._fragmented
|
||||||
next unless requested_attrs.nil? || requested_attrs.include?(name)
|
self.class._fragmented.read_attribute_for_serialization(attr)
|
||||||
if self.class._fragmented
|
else
|
||||||
hash[name] = self.class._fragmented.public_send(name)
|
object.read_attribute_for_serialization(attr)
|
||||||
else
|
|
||||||
hash[name] = send(name)
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -12,9 +12,10 @@ module ActiveModel
|
|||||||
|
|
||||||
DEFAULT_INCLUDE_TREE = ActiveModel::Serializer::IncludeTree.from_string('*')
|
DEFAULT_INCLUDE_TREE = ActiveModel::Serializer::IncludeTree.from_string('*')
|
||||||
|
|
||||||
included do |base|
|
included do
|
||||||
class << base
|
with_options instance_writer: false, instance_reader: true do |serializer|
|
||||||
attr_accessor :_reflections
|
serializer.class_attribute :_reflections
|
||||||
|
self._reflections ||= []
|
||||||
end
|
end
|
||||||
|
|
||||||
extend ActiveSupport::Autoload
|
extend ActiveSupport::Autoload
|
||||||
@ -29,7 +30,8 @@ module ActiveModel
|
|||||||
|
|
||||||
module ClassMethods
|
module ClassMethods
|
||||||
def inherited(base)
|
def inherited(base)
|
||||||
base._reflections = self._reflections.try(:dup) || []
|
super
|
||||||
|
base._reflections = _reflections.dup
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param [Symbol] name of the association
|
# @param [Symbol] name of the association
|
||||||
@ -39,8 +41,8 @@ module ActiveModel
|
|||||||
# @example
|
# @example
|
||||||
# has_many :comments, serializer: CommentSummarySerializer
|
# has_many :comments, serializer: CommentSummarySerializer
|
||||||
#
|
#
|
||||||
def has_many(name, options = {})
|
def has_many(name, options = {}, &block)
|
||||||
associate HasManyReflection.new(name, options)
|
associate(HasManyReflection.new(name, options, block))
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param [Symbol] name of the association
|
# @param [Symbol] name of the association
|
||||||
@ -50,8 +52,8 @@ module ActiveModel
|
|||||||
# @example
|
# @example
|
||||||
# belongs_to :author, serializer: AuthorSerializer
|
# belongs_to :author, serializer: AuthorSerializer
|
||||||
#
|
#
|
||||||
def belongs_to(name, options = {})
|
def belongs_to(name, options = {}, &block)
|
||||||
associate BelongsToReflection.new(name, options)
|
associate(BelongsToReflection.new(name, options, block))
|
||||||
end
|
end
|
||||||
|
|
||||||
# @param [Symbol] name of the association
|
# @param [Symbol] name of the association
|
||||||
@ -61,8 +63,8 @@ module ActiveModel
|
|||||||
# @example
|
# @example
|
||||||
# has_one :author, serializer: AuthorSerializer
|
# has_one :author, serializer: AuthorSerializer
|
||||||
#
|
#
|
||||||
def has_one(name, options = {})
|
def has_one(name, options = {}, &block)
|
||||||
associate HasOneReflection.new(name, options)
|
associate(HasOneReflection.new(name, options, block))
|
||||||
end
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
@ -76,10 +78,6 @@ module ActiveModel
|
|||||||
def associate(reflection)
|
def associate(reflection)
|
||||||
self._reflections = _reflections.dup
|
self._reflections = _reflections.dup
|
||||||
|
|
||||||
define_method reflection.name do
|
|
||||||
object.send reflection.name
|
|
||||||
end unless method_defined?(reflection.name)
|
|
||||||
|
|
||||||
self._reflections << reflection
|
self._reflections << reflection
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
112
lib/active_model/serializer/attributes.rb
Normal file
112
lib/active_model/serializer/attributes.rb
Normal file
@ -0,0 +1,112 @@
|
|||||||
|
module ActiveModel
|
||||||
|
class Serializer
|
||||||
|
module Attributes
|
||||||
|
# @api private
|
||||||
|
class Attribute
|
||||||
|
delegate :call, to: :reader
|
||||||
|
|
||||||
|
attr_reader :name, :reader
|
||||||
|
|
||||||
|
def initialize(name)
|
||||||
|
@name = name
|
||||||
|
@reader = :no_reader
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.build(name, block)
|
||||||
|
if block
|
||||||
|
AttributeBlock.new(name, block)
|
||||||
|
else
|
||||||
|
AttributeReader.new(name)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# @api private
|
||||||
|
class AttributeReader < Attribute
|
||||||
|
def initialize(name)
|
||||||
|
super(name)
|
||||||
|
@reader = ->(instance) { instance.read_attribute_for_serialization(name) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# @api private
|
||||||
|
class AttributeBlock < Attribute
|
||||||
|
def initialize(name, block)
|
||||||
|
super(name)
|
||||||
|
@reader = ->(instance) { instance.instance_eval(&block) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
extend ActiveSupport::Concern
|
||||||
|
|
||||||
|
included do
|
||||||
|
with_options instance_writer: false, instance_reader: false do |serializer|
|
||||||
|
serializer.class_attribute :_attribute_mappings # @api private : maps attribute key names to names to names of implementing methods, @see #attribute
|
||||||
|
self._attribute_mappings ||= {}
|
||||||
|
end
|
||||||
|
|
||||||
|
# Return the +attributes+ of +object+ as presented
|
||||||
|
# by the serializer.
|
||||||
|
def attributes(requested_attrs = nil, reload = false)
|
||||||
|
@attributes = nil if reload
|
||||||
|
@attributes ||= self.class._attribute_mappings.each_with_object({}) do |(key, attribute_mapping), hash|
|
||||||
|
next unless requested_attrs.nil? || requested_attrs.include?(key)
|
||||||
|
hash[key] = attribute_mapping.call(self)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
module ClassMethods
|
||||||
|
def inherited(base)
|
||||||
|
super
|
||||||
|
base._attribute_mappings = _attribute_mappings.dup
|
||||||
|
end
|
||||||
|
|
||||||
|
# @example
|
||||||
|
# class AdminAuthorSerializer < ActiveModel::Serializer
|
||||||
|
# attributes :id, :name, :recent_edits
|
||||||
|
def attributes(*attrs)
|
||||||
|
attrs = attrs.first if attrs.first.class == Array
|
||||||
|
|
||||||
|
attrs.each do |attr|
|
||||||
|
attribute(attr)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# @example
|
||||||
|
# class AdminAuthorSerializer < ActiveModel::Serializer
|
||||||
|
# attributes :id, :recent_edits
|
||||||
|
# attribute :name, key: :title
|
||||||
|
#
|
||||||
|
# attribute :full_name do
|
||||||
|
# "#{object.first_name} #{object.last_name}"
|
||||||
|
# end
|
||||||
|
#
|
||||||
|
# def recent_edits
|
||||||
|
# object.edits.last(5)
|
||||||
|
# end
|
||||||
|
def attribute(attr, options = {}, &block)
|
||||||
|
key = options.fetch(:key, attr)
|
||||||
|
_attribute_mappings[key] = Attribute.build(attr, block)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @api private
|
||||||
|
# names of attribute methods
|
||||||
|
# @see Serializer::attribute
|
||||||
|
def _attributes
|
||||||
|
_attribute_mappings.keys
|
||||||
|
end
|
||||||
|
|
||||||
|
# @api private
|
||||||
|
# maps attribute value to explict key name
|
||||||
|
# @see Serializer::attribute
|
||||||
|
# @see Adapter::FragmentCache#fragment_serializer
|
||||||
|
def _attributes_keys
|
||||||
|
_attribute_mappings
|
||||||
|
.each_with_object({}) do |(key, attribute_mapping), hash|
|
||||||
|
next if key == attribute_mapping.name
|
||||||
|
hash[attribute_mapping.name] = { key: key }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -7,8 +7,16 @@ module ActiveModel
|
|||||||
# class PostSerializer < ActiveModel::Serializer
|
# class PostSerializer < ActiveModel::Serializer
|
||||||
# has_one :author, serializer: AuthorSerializer
|
# has_one :author, serializer: AuthorSerializer
|
||||||
# has_many :comments
|
# has_many :comments
|
||||||
|
# has_many :comments, key: :last_comments do
|
||||||
|
# last(1)
|
||||||
|
# end
|
||||||
# end
|
# end
|
||||||
#
|
#
|
||||||
|
# Notice that the association block is evaluated in the context of the association.
|
||||||
|
# Specifically, the association 'comments' is evaluated two different ways:
|
||||||
|
# 1) as 'comments' and named 'comments'.
|
||||||
|
# 2) as 'comments.last(1)' and named 'last_comments'.
|
||||||
|
#
|
||||||
# PostSerializer._reflections #=>
|
# PostSerializer._reflections #=>
|
||||||
# # [
|
# # [
|
||||||
# # HasOneReflection.new(:author, serializer: AuthorSerializer),
|
# # HasOneReflection.new(:author, serializer: AuthorSerializer),
|
||||||
@ -17,7 +25,30 @@ module ActiveModel
|
|||||||
#
|
#
|
||||||
# So you can inspect reflections in your Adapters.
|
# So you can inspect reflections in your Adapters.
|
||||||
#
|
#
|
||||||
Reflection = Struct.new(:name, :options) do
|
Reflection = Struct.new(:name, :options, :block) do
|
||||||
|
delegate :call, to: :reader
|
||||||
|
|
||||||
|
attr_reader :reader
|
||||||
|
|
||||||
|
def initialize(*)
|
||||||
|
super
|
||||||
|
@reader = self.class.build_reader(name, block)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @api private
|
||||||
|
def value(instance)
|
||||||
|
call(instance)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @api private
|
||||||
|
def self.build_reader(name, block)
|
||||||
|
if block
|
||||||
|
->(instance) { instance.read_attribute_for_serialization(name).instance_eval(&block) }
|
||||||
|
else
|
||||||
|
->(instance) { instance.read_attribute_for_serialization(name) }
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
# Build association. This method is used internally to
|
# Build association. This method is used internally to
|
||||||
# build serializer's association by its reflection.
|
# build serializer's association by its reflection.
|
||||||
#
|
#
|
||||||
@ -40,7 +71,7 @@ module ActiveModel
|
|||||||
# @api private
|
# @api private
|
||||||
#
|
#
|
||||||
def build_association(subject, parent_serializer_options)
|
def build_association(subject, parent_serializer_options)
|
||||||
association_value = subject.send(name)
|
association_value = value(subject)
|
||||||
reflection_options = options.dup
|
reflection_options = options.dup
|
||||||
serializer_class = subject.class.serializer_for(association_value, reflection_options)
|
serializer_class = subject.class.serializer_for(association_value, reflection_options)
|
||||||
|
|
||||||
|
|||||||
2
test/fixtures/poro.rb
vendored
2
test/fixtures/poro.rb
vendored
@ -116,7 +116,7 @@ RoleSerializer = Class.new(ActiveModel::Serializer) do
|
|||||||
attributes :id, :name, :description, :slug
|
attributes :id, :name, :description, :slug
|
||||||
|
|
||||||
def slug
|
def slug
|
||||||
"#{name}-#{id}"
|
"#{object.name}-#{object.id}"
|
||||||
end
|
end
|
||||||
|
|
||||||
belongs_to :author
|
belongs_to :author
|
||||||
|
|||||||
@ -126,6 +126,35 @@ module ActiveModel
|
|||||||
assert expected_association_keys.include? :site
|
assert expected_association_keys.include? :site
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class InlineAssociationTestPostSerializer < ActiveModel::Serializer
|
||||||
|
has_many :comments
|
||||||
|
has_many :comments, key: :last_comments do
|
||||||
|
last(1)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_virtual_attribute_block
|
||||||
|
comment1 = ::ARModels::Comment.create!(contents: 'first comment')
|
||||||
|
comment2 = ::ARModels::Comment.create!(contents: 'last comment')
|
||||||
|
post = ::ARModels::Post.create!(
|
||||||
|
title: 'inline association test',
|
||||||
|
body: 'etc',
|
||||||
|
comments: [comment1, comment2]
|
||||||
|
)
|
||||||
|
actual = serializable(post, adapter: :attributes, serializer: InlineAssociationTestPostSerializer).as_json
|
||||||
|
expected = {
|
||||||
|
:comments => [
|
||||||
|
{ :id => 1, :contents => 'first comment' },
|
||||||
|
{ :id => 2, :contents => 'last comment' }
|
||||||
|
],
|
||||||
|
:last_comments => [
|
||||||
|
{ :id => 2, :contents => 'last comment' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
assert_equal expected, actual
|
||||||
|
end
|
||||||
|
|
||||||
class NamespacedResourcesTest < Minitest::Test
|
class NamespacedResourcesTest < Minitest::Test
|
||||||
class ResourceNamespace
|
class ResourceNamespace
|
||||||
Post = Class.new(::Model)
|
Post = Class.new(::Model)
|
||||||
|
|||||||
@ -43,6 +43,15 @@ module ActiveModel
|
|||||||
assert_equal({ blog: { id: 'AMS Hints' } }, adapter.serializable_hash)
|
assert_equal({ blog: { id: 'AMS Hints' } }, adapter.serializable_hash)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_object_attribute_override
|
||||||
|
serializer = Class.new(ActiveModel::Serializer) do
|
||||||
|
attribute :name, key: :object
|
||||||
|
end
|
||||||
|
|
||||||
|
adapter = ActiveModel::Serializer::Adapter::Json.new(serializer.new(@blog))
|
||||||
|
assert_equal({ blog: { object: 'AMS Hints' } }, adapter.serializable_hash)
|
||||||
|
end
|
||||||
|
|
||||||
def test_type_attribute
|
def test_type_attribute
|
||||||
attribute_serializer = Class.new(ActiveModel::Serializer) do
|
attribute_serializer = Class.new(ActiveModel::Serializer) do
|
||||||
attribute :id, key: :type
|
attribute :id, key: :type
|
||||||
@ -71,6 +80,21 @@ module ActiveModel
|
|||||||
|
|
||||||
assert_equal('custom', hash[:blog][:id])
|
assert_equal('custom', hash[:blog][:id])
|
||||||
end
|
end
|
||||||
|
|
||||||
|
PostWithVirtualAttribute = Class.new(::Model)
|
||||||
|
class PostWithVirtualAttributeSerializer < ActiveModel::Serializer
|
||||||
|
attribute :name do
|
||||||
|
"#{object.first_name} #{object.last_name}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_virtual_attribute_block
|
||||||
|
post = PostWithVirtualAttribute.new(first_name: 'Lucas', last_name: 'Hosseini')
|
||||||
|
hash = serializable(post).serializable_hash
|
||||||
|
expected = { name: 'Lucas Hosseini' }
|
||||||
|
|
||||||
|
assert_equal(expected, hash)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user