Skip to content
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
238 changes: 226 additions & 12 deletions lib/active_model/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,7 @@
require 'active_model/serializer/array_serializer'
require 'active_model/serializer/error_serializer'
require 'active_model/serializer/errors_serializer'
require 'active_model/serializer/concerns/associations'
require 'active_model/serializer/concerns/attributes'
require 'active_model/serializer/concerns/caching'
require 'active_model/serializer/concerns/configuration'
require 'active_model/serializer/concerns/links'
require 'active_model/serializer/concerns/meta'
require 'active_model/serializer/concerns/type'
require 'active_model/serializer/fieldset'
require 'active_model/serializer/lint'

Expand All @@ -23,13 +17,16 @@ class Serializer
extend ActiveSupport::Autoload
autoload :Adapter
autoload :Null
include Configuration
include Associations
include Attributes
autoload :Attribute
autoload :Association
autoload :Reflection
autoload :SingularReflection
autoload :CollectionReflection
autoload :BelongsToReflection
autoload :HasOneReflection
autoload :HasManyReflection
include ActiveSupport::Configurable
include Caching
include Links
include Meta
include Type

# @param resource [ActiveRecord::Base, ActiveModelSerializers::Model]
# @return [ActiveModel::Serializer]
Expand Down Expand Up @@ -111,6 +108,193 @@ def self.serialization_adapter_instance
@serialization_adapter_instance ||= ActiveModelSerializers::Adapter::Attributes
end

# Preferred interface is ActiveModelSerializers.config
# BEGIN DEFAULT CONFIGURATION
Copy link
Copy Markdown
Member Author

@bf4 bf4 Mar 16, 2017

Choose a reason for hiding this comment

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

This is a reminder that some of the configuration stuff in here is 'serializer'-specific, and some is global, which is why I moved the preferred interface to ActiveModelSerializers.config from ActiveModel::Serializer.config, but I think there's an argument that some aspects of inheritable config should remain in the serializer.

config.collection_serializer = ActiveModel::Serializer::CollectionSerializer
config.serializer_lookup_enabled = true
Copy link
Copy Markdown
Member Author


# @deprecated Use {#config.collection_serializer=} instead of this. Is
# compatibilty layer for ArraySerializer.
def config.array_serializer=(collection_serializer)
self.collection_serializer = collection_serializer
end

# @deprecated Use {#config.collection_serializer} instead of this. Is
# compatibilty layer for ArraySerializer.
def config.array_serializer
collection_serializer
end

config.default_includes = '*'
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

config.adapter = :attributes
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

See Adapter unification RFC and the difference between 'attributes' and 'json', in terms of serializing individual records, should be whether include_root_in_json or root is true. In terms of generating the response document, when root is true, then other options like meta could be processed; Otherwise, they should be ignored since there's no notion of a document (distinct from the serialized resource(s)) without a root, and hence no document-level meta.

config.key_transform = nil
config.jsonapi_pagination_links_enabled = true
config.jsonapi_resource_type = :plural
config.jsonapi_namespace_separator = '-'.freeze
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I was thinking... though we recommend -- now, why not .?

config.jsonapi_version = '1.0'
config.jsonapi_toplevel_meta = {}
# Make JSON API top-level jsonapi member opt-in
# ref: http://jsonapi.org/format/#document-top-level
config.jsonapi_include_toplevel_object = false
config.include_data_default = true
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

TODO: DOC, is the default for the include_data reflection option, ref https://github.com/rails-api/active_model_serializers/blob/ff27032720956f7c5cd9ef50618606cfebaa7ad9/docs/general/serializers.md#associations

  1. true (default)-- always include association data
  2. false -- never include association data
  3. :if_sideloaded (uses include_slice option passed to associations method, tl;dr include_slice.key?(reflection.name))


# For configuring how serializers are found.
# This should be an array of procs.
#
# The priority of the output is that the first item
# in the evaluated result array will take precedence
# over other possible serializer paths.
#
# i.e.: First match wins.
#
# @example output
# => [
# "CustomNamespace::ResourceSerializer",
# "ParentSerializer::ResourceSerializer",
# "ResourceNamespace::ResourceSerializer" ,
# "ResourceSerializer"]
#
# If CustomNamespace::ResourceSerializer exists, it will be used
# for serialization
config.serializer_lookup_chain = ActiveModelSerializers::LookupChain::DEFAULT.dup

config.schema_path = 'test/support/schemas'
# END DEFAULT CONFIGURATION

with_options instance_writer: false, instance_reader: false do |serializer|
serializer.class_attribute :_attributes_data # @api private
self._attributes_data ||= {}
end
with_options instance_writer: false, instance_reader: true do |serializer|
serializer.class_attribute :_reflections
self._reflections ||= {}
serializer.class_attribute :_links # @api private
self._links ||= {}
serializer.class_attribute :_meta # @api private
serializer.class_attribute :_type # @api private
end

def self.inherited(base)
super
base._attributes_data = _attributes_data.dup
base._reflections = _reflections.dup
base._links = _links.dup
end

# @return [Array<Symbol>] Key names of declared attributes
# @see Serializer::attribute
def self._attributes
_attributes_data.keys
end
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I moved this above the 'MACROS' section and removed @api private since it's been a public interface across multiple versions. I think we tagged it as private just to be conservative and let us pick a different method name, if we like.

Too bad _attributes returns an array of attribute names and _reflections returns a hash of association names to reflections (is more like _attributes_data)


# BEGIN SERIALIZER MACROS

# @example
# class AdminAuthorSerializer < ActiveModel::Serializer
# attributes :id, :name, :recent_edits
def self.attributes(*attrs)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

# BEGIN SERIALIZER MACROS

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 self.attribute(attr, options = {}, &block)
key = options.fetch(:key, attr)
_attributes_data[key] = Attribute.new(attr, options, block)
end

# @param [Symbol] name of the association
# @param [Hash<Symbol => any>] options for the reflection
# @return [void]
#
# @example
# has_many :comments, serializer: CommentSummarySerializer
#
def self.has_many(name, options = {}, &block) # rubocop:disable Style/PredicateName
Copy link
Copy Markdown
Member Author

@bf4 bf4 Mar 15, 2017

Choose a reason for hiding this comment

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

making blocks available to associations complicates the reflection since there's no way to know anything about what's in it until it's evaluated, and hence trying to just get the id/type for a belongs_to, and not loading the association, is harder. We should probably revisit what information we have when a block is passed.

See #1857 (and #2026 )

associate(HasManyReflection.new(name, options, block))
end

# @param [Symbol] name of the association
# @param [Hash<Symbol => any>] options for the reflection
# @return [void]
#
# @example
# belongs_to :author, serializer: AuthorSerializer
#
def self.belongs_to(name, options = {}, &block)
associate(BelongsToReflection.new(name, options, block))
end

# @param [Symbol] name of the association
# @param [Hash<Symbol => any>] options for the reflection
# @return [void]
#
# @example
# has_one :author, serializer: AuthorSerializer
#
def self.has_one(name, options = {}, &block) # rubocop:disable Style/PredicateName
associate(HasOneReflection.new(name, options, block))
end

# Add reflection and define {name} accessor.
# @param [ActiveModel::Serializer::Reflection] reflection
# @return [void]
#
# @api private
def self.associate(reflection)
key = reflection.options[:key] || reflection.name
self._reflections[key] = reflection
end
private_class_method :associate
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

NOTE for future PR:


# Define a link on a serializer.
# @example
# link(:self) { resource_url(object) }
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

# @example
# link(:self) { "http://example.com/resource/#{object.id}" }
# @example
# link :resource, "http://example.com/resource"
#
def self.link(name, value = nil, &block)
_links[name] = block || value
end

# Set the JSON API meta attribute of a serializer.
# @example
# class AdminAuthorSerializer < ActiveModel::Serializer
# meta { stuff: 'value' }
# @example
# meta do
# { comment_count: object.comments.count }
# end
def self.meta(value = nil, &block)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It should be noted that this is a serializer-level (resource object) meta, distinct from adapter-level (document)

self._meta = block || value
end

# Set the JSON API type of a serializer.
# @example
# class AdminAuthorSerializer < ActiveModel::Serializer
# type 'authors'
def self.type(type)
self._type = type && type.to_s
Copy link
Copy Markdown
Member Author

@bf4 bf4 Mar 15, 2017

Choose a reason for hiding this comment

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

resource roots esp as render options need review C: JSON Roots

Some thoughts I wrote in our Slack last September:

unifying the root key logic would be a huge boon,

from 1843 #1756 (comment) #1794 (comment)

summary of the issues: there's four methods that determine the resource root. This is especially confusing in the collection serializer. stay away until you need to, but can be useful in seeing the touch point. small working pieces > everything. so, any progress can be a pr.

summary of the goal: the main difference between the attributes and json adapters is the root option. look through the issues and prs and you'll see the trouble handling this at the adapter causes. If root could be turned on or off in the attributes or json adapter the same way, they might even be able to be rejoined. (They were split by joaomdmoura , and I've since moved a lot of the logic into the serializer)

There's lots of little pieces of this that are individually easish to find and fix. The overall goal is to just make it simple.

Naming: serializer type macro should be used to determine the root name, when present. The name can be over-ridden at the instance level either by passing in root or by defining a method named root. json_key is a bad name and should be deprecated. root is also kind of a bad name... actual method should probably be _type or something instead of root so as to not conflict with other instance methods and also for better parity. There's a lot of decisions to be made here.

so, within this there's 1) whether to include a root 2) where do we determine whether to declare a root (option, serializer, config, adapter)? 3) what should the value of the root be

end
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

# END SERIALIZER MACROS


# END SERIALIZER MACROS

attr_accessor :object, :root, :scope

# `scope_name` is set as :current_user by default in the controller.
Expand All @@ -131,6 +315,36 @@ def success?
true
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._attributes_data.each_with_object({}) do |(key, attr), hash|
next if attr.excluded?(self)
next unless requested_attrs.nil? || requested_attrs.include?(key)
hash[key] = attr.value(self)
end
end

# @param [JSONAPI::IncludeDirective] include_directive (defaults to the
# +default_include_directive+ config value when not provided)
# @return [Enumerator<Association>]
#
def associations(include_directive = ActiveModelSerializers.default_include_directive, include_slice = nil)
include_slice ||= include_directive
return unless object
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

❓ For another PR, should return Enumerator.new, right?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

yeah, it should be lazy, cause depending on the request / include args, maybe associations is never evaluated


Enumerator.new do |y|
self.class._reflections.values.each do |reflection|
next if reflection.excluded?(self)
key = reflection.options.fetch(:key, reflection.name)
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

NOTE: key should always be set since

    def self.associate(reflection)
      key = reflection.options[:key] || reflection.name
      self._reflections[key] = reflection
    end

which mean we could safely change .values.each do |reflection| to .each do |key, reflection|

next unless include_directive.key?(key)

y.yield reflection.build_association(self, instance_options, include_slice)
end
end
end

# @return [Hash] containing the attributes and first level
# associations, similar to how ActiveModel::Serializers::JSON is used
# in ActiveRecord::Base.
Expand Down
102 changes: 0 additions & 102 deletions lib/active_model/serializer/concerns/associations.rb

This file was deleted.

Loading