Merge branch 'master' into 0-10-stable

This commit is contained in:
Benjamin Fleischer 2017-05-01 11:04:46 -05:00
commit b48aeeef1e
37 changed files with 1317 additions and 651 deletions

View File

@ -1,10 +1,13 @@
AllCops: AllCops:
TargetRubyVersion: 2.1 TargetRubyVersion: 2.1
Exclude: Exclude:
- config/initializers/forbidden_yaml.rb
- !ruby/regexp /(vendor|bundle|bin|db|tmp)\/.*/ - !ruby/regexp /(vendor|bundle|bin|db|tmp)\/.*/
DisplayCopNames: true DisplayCopNames: true
DisplayStyleGuide: true DisplayStyleGuide: true
# https://github.com/bbatsov/rubocop/blob/master/manual/caching.md
# https://github.com/bbatsov/rubocop/blob/e8680418b351491e111a18cf5b453fc07a3c5239/config/default.yml#L60-L77
UseCache: true
CacheRootDirectory: tmp
Rails: Rails:
Enabled: true Enabled: true

View File

@ -1,6 +1,6 @@
## 0.10.x ## 0.10.x
### [master (unreleased)](https://github.com/rails-api/active_model_serializers/compare/v0.10.5...master) ### [master (unreleased)](https://github.com/rails-api/active_model_serializers/compare/v0.10.6...master)
Breaking changes: Breaking changes:
@ -14,6 +14,20 @@ Fixes:
Misc: Misc:
### [v0.10.6 (2017-05-01)](https://github.com/rails-api/active_model_serializers/compare/v0.10.5...v0.10.6)
Fixes:
- [#1857](https://github.com/rails-api/active_model_serializers/pull/1857) JSON:API does not load belongs_to relation to get identifier id. (@bf4)
- [#2119](https://github.com/rails-api/active_model_serializers/pull/2119) JSON:API returns null resource object identifier when 'id' is null. (@bf4)
- [#2093](https://github.com/rails-api/active_model_serializers/pull/2093) undef problematic Serializer methods: display, select. (@bf4)
Misc:
- [#2104](https://github.com/rails-api/active_model_serializers/pull/2104) Documentation for serializers and rendering. (@cassidycodes)
- [#2081](https://github.com/rails-api/active_model_serializers/pull/2081) Documentation for `include` option in adapters. (@charlie-wasp)
- [#2120](https://github.com/rails-api/active_model_serializers/pull/2120) Documentation for association options: foreign_key, type, class_name, namespace. (@bf4)
### [v0.10.5 (2017-03-07)](https://github.com/rails-api/active_model_serializers/compare/v0.10.4...v0.10.5) ### [v0.10.5 (2017-03-07)](https://github.com/rails-api/active_model_serializers/compare/v0.10.4...v0.10.5)
Breaking changes: Breaking changes:
@ -81,7 +95,7 @@ Misc:
- [#1878](https://github.com/rails-api/active_model_serializers/pull/1878) Cache key generation for serializers now uses `ActiveSupport::Cache.expand_cache_key` instead of `Array#join` by default and is also overridable. This change should be backward-compatible. (@markiz) - [#1878](https://github.com/rails-api/active_model_serializers/pull/1878) Cache key generation for serializers now uses `ActiveSupport::Cache.expand_cache_key` instead of `Array#join` by default and is also overridable. This change should be backward-compatible. (@markiz)
- [#1799](https://github.com/rails-api/active_model_serializers/pull/1799) Add documentation for setting the adapter. (@ScottKbka) - [#1799](https://github.com/rails-api/active_model_serializers/pull/1799) Add documentation for setting the adapter. (@cassidycodes)
- [#1909](https://github.com/rails-api/active_model_serializers/pull/1909) Add documentation for relationship links. (@vasilakisfil, @NullVoxPopuli) - [#1909](https://github.com/rails-api/active_model_serializers/pull/1909) Add documentation for relationship links. (@vasilakisfil, @NullVoxPopuli)
- [#1959](https://github.com/rails-api/active_model_serializers/pull/1959) Add documentation for root. (@shunsuke227ono) - [#1959](https://github.com/rails-api/active_model_serializers/pull/1959) Add documentation for root. (@shunsuke227ono)
- [#1967](https://github.com/rails-api/active_model_serializers/pull/1967) Improve type method documentation. (@yukideluxe) - [#1967](https://github.com/rails-api/active_model_serializers/pull/1967) Improve type method documentation. (@yukideluxe)

View File

@ -90,8 +90,8 @@ reading documentation for our `master`, which may include features that have not
been released yet. Please see below for the documentation relevant to you. been released yet. Please see below for the documentation relevant to you.
- [0.10 (master) Documentation](https://github.com/rails-api/active_model_serializers/tree/master) - [0.10 (master) Documentation](https://github.com/rails-api/active_model_serializers/tree/master)
- [0.10.4 (latest release) Documentation](https://github.com/rails-api/active_model_serializers/tree/v0.10.4) - [0.10.6 (latest release) Documentation](https://github.com/rails-api/active_model_serializers/tree/v0.10.6)
- [![API Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/gems/active_model_serializers/0.10.4) - [![API Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/gems/active_model_serializers/0.10.6)
- [Guides](docs) - [Guides](docs)
- [0.9 (0-9-stable) Documentation](https://github.com/rails-api/active_model_serializers/tree/0-9-stable) - [0.9 (0-9-stable) Documentation](https://github.com/rails-api/active_model_serializers/tree/0-9-stable)
- [![API Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/github/rails-api/active_model_serializers/0-9-stable) - [![API Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/github/rails-api/active_model_serializers/0-9-stable)

View File

@ -7,6 +7,7 @@ begin
require 'simplecov' require 'simplecov'
rescue LoadError # rubocop:disable Lint/HandleExceptions rescue LoadError # rubocop:disable Lint/HandleExceptions
end end
import('lib/tasks/rubocop.rake')
Bundler::GemHelper.install_tasks Bundler::GemHelper.install_tasks
@ -30,36 +31,6 @@ namespace :yard do
end end
end end
begin
require 'rubocop'
require 'rubocop/rake_task'
rescue LoadError # rubocop:disable Lint/HandleExceptions
else
Rake::Task[:rubocop].clear if Rake::Task.task_defined?(:rubocop)
require 'rbconfig'
# https://github.com/bundler/bundler/blob/1b3eb2465a/lib/bundler/constants.rb#L2
windows_platforms = /(msdos|mswin|djgpp|mingw)/
if RbConfig::CONFIG['host_os'] =~ windows_platforms
desc 'No-op rubocop on Windows-- unsupported platform'
task :rubocop do
puts 'Skipping rubocop on Windows'
end
elsif defined?(::Rubinius)
desc 'No-op rubocop to avoid rbx segfault'
task :rubocop do
puts 'Skipping rubocop on rbx due to segfault'
puts 'https://github.com/rubinius/rubinius/issues/3499'
end
else
Rake::Task[:rubocop].clear if Rake::Task.task_defined?(:rubocop)
desc 'Execute rubocop'
RuboCop::RakeTask.new(:rubocop) do |task|
task.options = ['--rails', '--display-cop-names', '--display-style-guide']
task.fail_on_error = true
end
end
end
require 'rake/testtask' require 'rake/testtask'
Rake::TestTask.new(:test) do |t| Rake::TestTask.new(:test) do |t|

View File

@ -57,7 +57,7 @@ Gem::Specification.new do |spec|
spec.add_development_dependency 'bundler', '~> 1.6' spec.add_development_dependency 'bundler', '~> 1.6'
spec.add_development_dependency 'simplecov', '~> 0.11' spec.add_development_dependency 'simplecov', '~> 0.11'
spec.add_development_dependency 'timecop', '~> 0.7' spec.add_development_dependency 'timecop', '~> 0.7'
spec.add_development_dependency 'grape', ['>= 0.13', '< 1.0'] spec.add_development_dependency 'grape', ['>= 0.13', '< 0.19.1']
spec.add_development_dependency 'json_schema' spec.add_development_dependency 'json_schema'
spec.add_development_dependency 'rake', ['>= 10.0', '< 12.0'] spec.add_development_dependency 'rake', ['>= 10.0', '< 12.0']
end end

38
bin/rubocop Executable file
View File

@ -0,0 +1,38 @@
#!/usr/bin/env bash
#
# Usage:
# bin/rubocop [-A|-t|-h]
# bin/rubocop [file or path] [cli options]
#
# Options:
# Autocorrect -A
# AutoGenConfig -t
# Usage -h,--help,help
set -e
case $1 in
-A)
echo "Rubocop autocorrect is ON" >&2
bundle exec rake -f lib/tasks/rubocop.rake rubocop:auto_correct
;;
-t)
echo "Rubocop is generating a new TODO" >&2
bundle exec rake -f lib/tasks/rubocop.rake rubocop:auto_gen_config
;;
-h|--help|help)
sed -ne '/^#/!q;s/.\{1,2\}//;1d;p' < "$0"
;;
*)
# with no args, run vanilla rubocop
# else assume we're passing in arbitrary arguments
if [ -z "$1" ]; then
bundle exec rake -f lib/tasks/rubocop.rake rubocop
else
bundle exec rubocop "$@"
fi
;;
esac

View File

@ -141,18 +141,25 @@ This adapter follows **version 1.0** of the [format specified](../jsonapi/schema
} }
``` ```
#### Included ### Include option
It will include the associated resources in the `"included"` member Which [serializer associations](https://github.com/rails-api/active_model_serializers/blob/master/docs/general/serializers.md#associations) are rendered can be specified using the `include` option. The option usage is consistent with [the include option in the JSON API spec](http://jsonapi.org/format/#fetching-includes), and is available in all adapters.
when the resource names are included in the `include` option.
Including nested associated resources is also supported.
Example of the usage:
```ruby ```ruby
render json: @posts, include: ['author', 'comments', 'comments.author'] render json: @posts, include: ['author', 'comments', 'comments.author']
# or # or
render json: @posts, include: 'author,comments,comments.author' render json: @posts, include: 'author,comments,comments.author'
``` ```
The format of the `include` option can be either:
- a String composed of a comma-separated list of [relationship paths](http://jsonapi.org/format/#fetching-includes).
- an Array of Symbols and Hashes.
- a mix of both.
An empty string or an empty array will prevent rendering of any associations.
In addition, two types of wildcards may be used: In addition, two types of wildcards may be used:
- `*` includes one level of associations. - `*` includes one level of associations.
@ -164,11 +171,6 @@ These can be combined with other paths.
render json: @posts, include: '**' # or '*' for a single layer render json: @posts, include: '**' # or '*' for a single layer
``` ```
The format of the `include` option can be either:
- a String composed of a comma-separated list of [relationship paths](http://jsonapi.org/format/#fetching-includes).
- an Array of Symbols and Hashes.
- a mix of both.
The following would render posts and include: The following would render posts and include:
@ -182,6 +184,20 @@ It could be combined, like above, with other paths in any combination desired.
render json: @posts, include: 'author.comments.**' render json: @posts, include: 'author.comments.**'
``` ```
**Note:** Wildcards are ActiveModelSerializers-specific, they are not part of the JSON API spec.
The default include for the JSON API adapter is no associations. The default for the JSON and Attributes adapters is all associations.
For the JSON API adapter associated resources will be gathered in the `"included"` member. For the JSON and Attributes
adapters associated resources will be rendered among the other attributes.
Only for the JSON API adapter you can specify, which attributes of associated resources will be rendered. This feature
is called [sparse fieldset](http://jsonapi.org/format/#fetching-sparse-fieldsets):
```ruby
render json: @posts, include: 'comments', fields: { comments: ['content', 'created_at'] }
```
##### Security Considerations ##### Security Considerations
Since the included options may come from the query params (i.e. user-controller): Since the included options may come from the query params (i.e. user-controller):

View File

@ -37,7 +37,7 @@ and
class CommentSerializer < ActiveModel::Serializer class CommentSerializer < ActiveModel::Serializer
attributes :name, :body attributes :name, :body
belongs_to :post_id belongs_to :post
end end
``` ```

View File

@ -203,7 +203,7 @@ link(:link_name) { url_for(controller: 'controller_name', action: 'index', only_
#### include #### include
PR please :) See [Adapters: Include Option](/docs/general/adapters.md#include-option).
#### Overriding the root key #### Overriding the root key
@ -260,15 +260,29 @@ Note that by using a string and symbol, Ruby will assume the namespace is define
#### serializer #### serializer
PR please :) Specify which serializer to use if you want to use a serializer other than the default.
For a single resource:
```ruby
@post = Post.first
render json: @post, serializer: SpecialPostSerializer
```
To specify which serializer to use on individual items in a collection (i.e., an `index` action), use `each_serializer`:
```ruby
@posts = Post.all
render json: @posts, each_serializer: SpecialPostSerializer
```
#### scope #### scope
PR please :) See [Serializers: Scope](/docs/general/serializers.md#scope).
#### scope_name #### scope_name
PR please :) See [Serializers: Scope](/docs/general/serializers.md#scope).
## Using a serializer without `render` ## Using a serializer without `render`

View File

@ -64,6 +64,10 @@ Where:
- `unless:` - `unless:`
- `virtual_value:` - `virtual_value:`
- `polymorphic:` defines if polymorphic relation type should be nested in serialized association. - `polymorphic:` defines if polymorphic relation type should be nested in serialized association.
- `type:` the resource type as used by JSON:API, especially on a `belongs_to` relationship.
- `class_name:` used to determine `type` when `type` not given
- `foreign_key:` used by JSON:API on a `belongs_to` relationship to avoid unnecessarily loading the association object.
- `namespace:` used when looking up the serializer and `serializer` is not given. Falls back to the parent serializer's `:namespace` instance options, which, when present, comes from the render options. See [Rendering#namespace](rendering.md#namespace] for more details.
- optional: `&block` is a context that returns the association's attributes. - optional: `&block` is a context that returns the association's attributes.
- prevents `association_name` method from being called. - prevents `association_name` method from being called.
- return value of block is used as the association value. - return value of block is used as the association value.
@ -382,11 +386,26 @@ The serialized value for a given key. e.g. `read_attribute_for_serialization(:ti
#### #links #### #links
PR please :) Allows you to modify the `links` node. By default, this node will be populated with the attributes set using the [::link](#link) method. Using `links: nil` will remove the `links` node.
```ruby
ActiveModelSerializers::SerializableResource.new(
@post,
adapter: :json_api,
links: {
self: {
href: 'http://example.com/posts',
meta: {
stuff: 'value'
}
}
}
)
```
#### #json_key #### #json_key
PR please :) Returns the key used by the adapter as the resource root. See [root](#root) for more information.
## Examples ## Examples

View File

@ -72,7 +72,7 @@ ActiveModelSerializers pagination relies on a paginated collection with the meth
### JSON adapter ### JSON adapter
If you are using `JSON` adapter, pagination links will not be included automatically, but it is possible to do so using `meta` key. If you are not using `JSON` adapter, pagination links will not be included automatically, but it is possible to do so using `meta` key.
Add this method to your base API controller. Add this method to your base API controller.

View File

@ -74,6 +74,9 @@ Then, in your controller you can tell rails you're accepting and rendering the j
end end
``` ```
#### Note:
In Rails 5, the "unsafe" method ( `jsonapi_parse!` vs the safe `jsonapi_parse`) throws an `InvalidDocument` exception when the payload does not meet basic criteria for JSON API deserialization.
### Adapter Changes ### Adapter Changes

View File

@ -4,13 +4,7 @@ require 'active_model/serializer/collection_serializer'
require 'active_model/serializer/array_serializer' require 'active_model/serializer/array_serializer'
require 'active_model/serializer/error_serializer' require 'active_model/serializer/error_serializer'
require 'active_model/serializer/errors_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/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/fieldset'
require 'active_model/serializer/lint' require 'active_model/serializer/lint'
@ -18,33 +12,40 @@ require 'active_model/serializer/lint'
# reified when subclassed to decorate a resource. # reified when subclassed to decorate a resource.
module ActiveModel module ActiveModel
class Serializer class Serializer
undef_method :select, :display # These IO methods, which are mixed into Kernel,
# sometimes conflict with attribute names. We don't need these IO methods.
# @see #serializable_hash for more details on these valid keys. # @see #serializable_hash for more details on these valid keys.
SERIALIZABLE_HASH_VALID_KEYS = [:only, :except, :methods, :include, :root].freeze SERIALIZABLE_HASH_VALID_KEYS = [:only, :except, :methods, :include, :root].freeze
extend ActiveSupport::Autoload extend ActiveSupport::Autoload
autoload :Adapter autoload :Adapter
autoload :Null autoload :Null
include Configuration autoload :Attribute
include Associations autoload :Association
include Attributes autoload :Reflection
autoload :SingularReflection
autoload :CollectionReflection
autoload :BelongsToReflection
autoload :HasOneReflection
autoload :HasManyReflection
include ActiveSupport::Configurable
include Caching include Caching
include Links
include Meta
include Type
# @param resource [ActiveRecord::Base, ActiveModelSerializers::Model] # @param resource [ActiveRecord::Base, ActiveModelSerializers::Model]
# @return [ActiveModel::Serializer] # @return [ActiveModel::Serializer]
# Preferentially returns # Preferentially returns
# 1. resource.serializer # 1. resource.serializer_class
# 2. ArraySerializer when resource is a collection # 2. ArraySerializer when resource is a collection
# 3. options[:serializer] # 3. options[:serializer]
# 4. lookup serializer when resource is a Class # 4. lookup serializer when resource is a Class
def self.serializer_for(resource, options = {}) def self.serializer_for(resource_or_class, options = {})
if resource.respond_to?(:serializer_class) if resource_or_class.respond_to?(:serializer_class)
resource.serializer_class resource_or_class.serializer_class
elsif resource.respond_to?(:to_ary) elsif resource_or_class.respond_to?(:to_ary)
config.collection_serializer config.collection_serializer
else else
options.fetch(:serializer) { get_serializer_for(resource.class, options[:namespace]) } resource_class = resource_or_class.class == Class ? resource_or_class : resource_or_class.class
options.fetch(:serializer) { get_serializer_for(resource_class, options[:namespace]) }
end end
end end
@ -91,6 +92,8 @@ module ActiveModel
serializer_class serializer_class
elsif klass.superclass elsif klass.superclass
get_serializer_for(klass.superclass) get_serializer_for(klass.superclass)
else
nil # No serializer found
end end
end end
end end
@ -111,6 +114,193 @@ module ActiveModel
@serialization_adapter_instance ||= ActiveModelSerializers::Adapter::Attributes @serialization_adapter_instance ||= ActiveModelSerializers::Adapter::Attributes
end end
# Preferred interface is ActiveModelSerializers.config
# BEGIN DEFAULT CONFIGURATION
config.collection_serializer = ActiveModel::Serializer::CollectionSerializer
config.serializer_lookup_enabled = true
# @deprecated Use {#config.collection_serializer=} instead of this. Is
# compatibility layer for ArraySerializer.
def config.array_serializer=(collection_serializer)
self.collection_serializer = collection_serializer
end
# @deprecated Use {#config.collection_serializer} instead of this. Is
# compatibility layer for ArraySerializer.
def config.array_serializer
collection_serializer
end
config.default_includes = '*'
config.adapter = :attributes
config.key_transform = nil
config.jsonapi_pagination_links_enabled = true
config.jsonapi_resource_type = :plural
config.jsonapi_namespace_separator = '-'.freeze
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
# 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
# BEGIN SERIALIZER MACROS
# @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
#
# 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
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
# Define a link on a serializer.
# @example
# link(:self) { resource_url(object) }
# @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)
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
end
# END SERIALIZER MACROS
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.
@ -131,53 +321,49 @@ module ActiveModel
true true
end 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 Enumerator.new unless object
Enumerator.new do |y|
self.class._reflections.each do |key, reflection|
next if reflection.excluded?(self)
next unless include_directive.key?(key)
association = reflection.build_association(self, instance_options, include_slice)
y.yield association
end
end
end
# @return [Hash] containing the attributes and first level # @return [Hash] containing the attributes and first level
# associations, similar to how ActiveModel::Serializers::JSON is used # associations, similar to how ActiveModel::Serializers::JSON is used
# in ActiveRecord::Base. # in ActiveRecord::Base.
#
# TODO: Include <tt>ActiveModel::Serializers::JSON</tt>.
# So that the below is true:
# @param options [nil, Hash] The same valid options passed to `serializable_hash`
# (:only, :except, :methods, and :include).
#
# See
# https://github.com/rails/rails/blob/v5.0.0.beta2/activemodel/lib/active_model/serializers/json.rb#L17-L101
# https://github.com/rails/rails/blob/v5.0.0.beta2/activemodel/lib/active_model/serialization.rb#L85-L123
# https://github.com/rails/rails/blob/v5.0.0.beta2/activerecord/lib/active_record/serialization.rb#L11-L17
# https://github.com/rails/rails/blob/v5.0.0.beta2/activesupport/lib/active_support/core_ext/object/json.rb#L147-L162
#
# @example
# # The :only and :except options can be used to limit the attributes included, and work
# # similar to the attributes method.
# serializer.as_json(only: [:id, :name])
# serializer.as_json(except: [:id, :created_at, :age])
#
# # To include the result of some method calls on the model use :methods:
# serializer.as_json(methods: :permalink)
#
# # To include associations use :include:
# serializer.as_json(include: :posts)
# # Second level and higher order associations work as well:
# serializer.as_json(include: { posts: { include: { comments: { only: :body } }, only: :title } })
def serializable_hash(adapter_options = nil, options = {}, adapter_instance = self.class.serialization_adapter_instance) def serializable_hash(adapter_options = nil, options = {}, adapter_instance = self.class.serialization_adapter_instance)
adapter_options ||= {} adapter_options ||= {}
options[:include_directive] ||= ActiveModel::Serializer.include_directive_from_options(adapter_options) options[:include_directive] ||= ActiveModel::Serializer.include_directive_from_options(adapter_options)
cached_attributes = adapter_options[:cached_attributes] ||= {} resource = attributes_hash(adapter_options, options, adapter_instance)
resource = fetch_attributes(options[:fields], cached_attributes, adapter_instance) relationships = associations_hash(adapter_options, options, adapter_instance)
relationships = resource_relationships(adapter_options, options, adapter_instance)
resource.merge(relationships) resource.merge(relationships)
end end
alias to_hash serializable_hash alias to_hash serializable_hash
alias to_h serializable_hash alias to_h serializable_hash
# @see #serializable_hash # @see #serializable_hash
# TODO: When moving attributes adapter logic here, @see #serializable_hash
# So that the below is true:
# @param options [nil, Hash] The same valid options passed to `as_json`
# (:root, :only, :except, :methods, and :include).
# The default for `root` is nil.
# The default value for include_root is false. You can change it to true if the given
# JSON string includes a single root node.
def as_json(adapter_opts = nil) def as_json(adapter_opts = nil)
serializable_hash(adapter_opts) serializable_hash(adapter_opts)
end end
@ -196,32 +382,24 @@ module ActiveModel
end end
# @api private # @api private
def resource_relationships(adapter_options, options, adapter_instance) def attributes_hash(_adapter_options, options, adapter_instance)
relationships = {} if self.class.cache_enabled?
include_directive = options.fetch(:include_directive) fetch_attributes(options[:fields], options[:cached_attributes] || {}, adapter_instance)
associations(include_directive).each do |association| elsif self.class.fragment_cache_enabled?
adapter_opts = adapter_options.merge(include_directive: include_directive[association.key]) fetch_attributes_fragment(adapter_instance, options[:cached_attributes] || {})
relationships[association.key] ||= relationship_value_for(association, adapter_opts, adapter_instance) else
attributes(options[:fields], true)
end end
relationships
end end
# @api private # @api private
def relationship_value_for(association, adapter_options, adapter_instance) def associations_hash(adapter_options, options, adapter_instance)
return association.options[:virtual_value] if association.options[:virtual_value] include_directive = options.fetch(:include_directive)
association_serializer = association.serializer include_slice = options[:include_slice]
association_object = association_serializer && association_serializer.object associations(include_directive, include_slice).each_with_object({}) do |association, relationships|
return unless association_object adapter_opts = adapter_options.merge(include_directive: include_directive[association.key], adapter_instance: adapter_instance)
relationships[association.key] = association.serializable_hash(adapter_opts, adapter_instance)
relationship_value = association_serializer.serializable_hash(adapter_options, {}, adapter_instance)
if association.options[:polymorphic] && relationship_value
polymorphic_type = association_object.class.name.underscore
relationship_value = { type: polymorphic_type, polymorphic_type.to_sym => relationship_value }
end end
relationship_value
end end
protected protected

View File

@ -1,34 +1,71 @@
require 'active_model/serializer/lazy_association'
module ActiveModel module ActiveModel
class Serializer class Serializer
# This class holds all information about serializer's association. # This class holds all information about serializer's association.
# #
# @attr [Symbol] name # @api private
# @attr [Hash{Symbol => Object}] options Association = Struct.new(:reflection, :association_options) do
# @attr [block] attr_reader :lazy_association
# delegate :object, :include_data?, :virtual_value, :collection?, to: :lazy_association
# @example
# Association.new(:comments, { serializer: CommentSummarySerializer }) def initialize(*)
# super
class Association < Field @lazy_association = LazyAssociation.new(reflection, association_options)
# @return [Symbol]
def key
options.fetch(:key, name)
end end
# @return [ActiveModel::Serializer, nil] # @return [Symbol]
def serializer delegate :name, to: :reflection
options[:serializer]
# @return [Symbol]
def key
reflection_options.fetch(:key, name)
end
# @return [True,False]
def key?
reflection_options.key?(:key)
end end
# @return [Hash] # @return [Hash]
def links def links
options.fetch(:links) || {} reflection_options.fetch(:links) || {}
end end
# @return [Hash, nil] # @return [Hash, nil]
# This gets mutated, so cannot use the cached reflection_options
def meta def meta
options[:meta] reflection.options[:meta]
end end
def belongs_to?
reflection.foreign_key_on == :self
end
def polymorphic?
true == reflection_options[:polymorphic]
end
# @api private
def serializable_hash(adapter_options, adapter_instance)
association_serializer = lazy_association.serializer
return virtual_value if virtual_value
association_object = association_serializer && association_serializer.object
return unless association_object
serialization = association_serializer.serializable_hash(adapter_options, {}, adapter_instance)
if polymorphic? && serialization
polymorphic_type = association_object.class.name.underscore
serialization = { type: polymorphic_type, polymorphic_type.to_sym => serialization }
end
serialization
end
private
delegate :reflection_options, to: :lazy_association
end end
end end
end end

View File

@ -1,7 +1,11 @@
module ActiveModel module ActiveModel
class Serializer class Serializer
# @api private # @api private
class BelongsToReflection < SingularReflection class BelongsToReflection < Reflection
# @api private
def foreign_key_on
:self
end
end end
end end
end end

View File

@ -1,7 +0,0 @@
module ActiveModel
class Serializer
# @api private
class CollectionReflection < Reflection
end
end
end

View File

@ -1,102 +0,0 @@
module ActiveModel
class Serializer
# Defines an association in the object should be rendered.
#
# The serializer object should implement the association name
# as a method which should return an array when invoked. If a method
# with the association name does not exist, the association name is
# dispatched to the serialized object.
#
module Associations
extend ActiveSupport::Concern
included do
with_options instance_writer: false, instance_reader: true do |serializer|
serializer.class_attribute :_reflections
self._reflections ||= {}
end
extend ActiveSupport::Autoload
autoload :Association
autoload :Reflection
autoload :SingularReflection
autoload :CollectionReflection
autoload :BelongsToReflection
autoload :HasOneReflection
autoload :HasManyReflection
end
module ClassMethods
def inherited(base)
super
base._reflections = _reflections.dup
end
# @param [Symbol] name of the association
# @param [Hash<Symbol => any>] options for the reflection
# @return [void]
#
# @example
# has_many :comments, serializer: CommentSummarySerializer
#
def has_many(name, options = {}, &block) # rubocop:disable Style/PredicateName
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 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 has_one(name, options = {}, &block) # rubocop:disable Style/PredicateName
associate(HasOneReflection.new(name, options, block))
end
private
# Add reflection and define {name} accessor.
# @param [ActiveModel::Serializer::Reflection] reflection
# @return [void]
#
# @api private
#
def associate(reflection)
key = reflection.options[:key] || reflection.name
self._reflections[key] = reflection
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
Enumerator.new do |y|
self.class._reflections.values.each do |reflection|
next if reflection.excluded?(self)
key = reflection.options.fetch(:key, reflection.name)
next unless include_directive.key?(key)
y.yield reflection.build_association(self, instance_options, include_slice)
end
end
end
end
end
end

View File

@ -1,82 +0,0 @@
module ActiveModel
class Serializer
module Attributes
extend ActiveSupport::Concern
included do
with_options instance_writer: false, instance_reader: false do |serializer|
serializer.class_attribute :_attributes_data # @api private
self._attributes_data ||= {}
end
extend ActiveSupport::Autoload
autoload :Attribute
# 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
end
module ClassMethods
def inherited(base)
super
base._attributes_data = _attributes_data.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)
_attributes_data[key] = Attribute.new(attr, options, block)
end
# @api private
# keys of attributes
# @see Serializer::attribute
def _attributes
_attributes_data.keys
end
# @api private
# maps attribute value to explicit key name
# @see Serializer::attribute
# @see FragmentCache#fragment_serializer
def _attributes_keys
_attributes_data
.each_with_object({}) do |(key, attr), hash|
next if key == attr.name
hash[attr.name] = { key: key }
end
end
end
end
end
end

View File

@ -40,9 +40,9 @@ module ActiveModel
module ClassMethods module ClassMethods
def inherited(base) def inherited(base)
super
caller_line = caller[1] caller_line = caller[1]
base._cache_digest_file_path = caller_line base._cache_digest_file_path = caller_line
super
end end
def _cache_digest def _cache_digest
@ -68,6 +68,18 @@ module ActiveModel
_cache_options && _cache_options[:skip_digest] _cache_options && _cache_options[:skip_digest]
end end
# @api private
# maps attribute value to explicit key name
# @see Serializer::attribute
# @see Serializer::fragmented_attributes
def _attributes_keys
_attributes_data
.each_with_object({}) do |(key, attr), hash|
next if key == attr.name
hash[attr.name] = { key: key }
end
end
def fragmented_attributes def fragmented_attributes
cached = _cache_only ? _cache_only : _attributes - _cache_except cached = _cache_only ? _cache_only : _attributes - _cache_except
cached = cached.map! { |field| _attributes_keys.fetch(field, field) } cached = cached.map! { |field| _attributes_keys.fetch(field, field) }
@ -158,6 +170,7 @@ module ActiveModel
# Read cache from cache_store # Read cache from cache_store
# @return [Hash] # @return [Hash]
# Used in CollectionSerializer to set :cached_attributes
def cache_read_multi(collection_serializer, adapter_instance, include_directive) def cache_read_multi(collection_serializer, adapter_instance, include_directive)
return {} if ActiveModelSerializers.config.cache_store.blank? return {} if ActiveModelSerializers.config.cache_store.blank?
@ -180,12 +193,14 @@ module ActiveModel
cache_keys << object_cache_key(serializer, adapter_instance) cache_keys << object_cache_key(serializer, adapter_instance)
serializer.associations(include_directive).each do |association| serializer.associations(include_directive).each do |association|
if association.serializer.respond_to?(:each) # TODO(BF): Process relationship without evaluating lazy_association
association.serializer.each do |sub_serializer| association_serializer = association.lazy_association.serializer
if association_serializer.respond_to?(:each)
association_serializer.each do |sub_serializer|
cache_keys << object_cache_key(sub_serializer, adapter_instance) cache_keys << object_cache_key(sub_serializer, adapter_instance)
end end
else else
cache_keys << object_cache_key(association.serializer, adapter_instance) cache_keys << object_cache_key(association_serializer, adapter_instance)
end end
end end
end end
@ -203,23 +218,18 @@ module ActiveModel
### INSTANCE METHODS ### INSTANCE METHODS
def fetch_attributes(fields, cached_attributes, adapter_instance) def fetch_attributes(fields, cached_attributes, adapter_instance)
if serializer_class.cache_enabled? key = cache_key(adapter_instance)
key = cache_key(adapter_instance) cached_attributes.fetch(key) do
cached_attributes.fetch(key) do fetch(adapter_instance, serializer_class._cache_options, key) do
serializer_class.cache_store.fetch(key, serializer_class._cache_options) do attributes(fields, true)
attributes(fields, true)
end
end end
elsif serializer_class.fragment_cache_enabled?
fetch_attributes_fragment(adapter_instance, cached_attributes)
else
attributes(fields, true)
end end
end end
def fetch(adapter_instance, cache_options = serializer_class._cache_options) def fetch(adapter_instance, cache_options = serializer_class._cache_options, key = nil)
if serializer_class.cache_store if serializer_class.cache_store
serializer_class.cache_store.fetch(cache_key(adapter_instance), cache_options) do key ||= cache_key(adapter_instance)
serializer_class.cache_store.fetch(key, cache_options) do
yield yield
end end
else else
@ -230,7 +240,6 @@ module ActiveModel
# 1. Determine cached fields from serializer class options # 1. Determine cached fields from serializer class options
# 2. Get non_cached_fields and fetch cache_fields # 2. Get non_cached_fields and fetch cache_fields
# 3. Merge the two hashes using adapter_instance#fragment_cache # 3. Merge the two hashes using adapter_instance#fragment_cache
# rubocop:disable Metrics/AbcSize
def fetch_attributes_fragment(adapter_instance, cached_attributes = {}) def fetch_attributes_fragment(adapter_instance, cached_attributes = {})
serializer_class._cache_options ||= {} serializer_class._cache_options ||= {}
serializer_class._cache_options[:key] = serializer_class._cache_key if serializer_class._cache_key serializer_class._cache_options[:key] = serializer_class._cache_key if serializer_class._cache_key
@ -239,22 +248,21 @@ module ActiveModel
non_cached_fields = fields[:non_cached].dup non_cached_fields = fields[:non_cached].dup
non_cached_hash = attributes(non_cached_fields, true) non_cached_hash = attributes(non_cached_fields, true)
include_directive = JSONAPI::IncludeDirective.new(non_cached_fields - non_cached_hash.keys) include_directive = JSONAPI::IncludeDirective.new(non_cached_fields - non_cached_hash.keys)
non_cached_hash.merge! resource_relationships({}, { include_directive: include_directive }, adapter_instance) non_cached_hash.merge! associations_hash({}, { include_directive: include_directive }, adapter_instance)
cached_fields = fields[:cached].dup cached_fields = fields[:cached].dup
key = cache_key(adapter_instance) key = cache_key(adapter_instance)
cached_hash = cached_hash =
cached_attributes.fetch(key) do cached_attributes.fetch(key) do
serializer_class.cache_store.fetch(key, serializer_class._cache_options) do fetch(adapter_instance, serializer_class._cache_options, key) do
hash = attributes(cached_fields, true) hash = attributes(cached_fields, true)
include_directive = JSONAPI::IncludeDirective.new(cached_fields - hash.keys) include_directive = JSONAPI::IncludeDirective.new(cached_fields - hash.keys)
hash.merge! resource_relationships({}, { include_directive: include_directive }, adapter_instance) hash.merge! associations_hash({}, { include_directive: include_directive }, adapter_instance)
end end
end end
# Merge both results # Merge both results
adapter_instance.fragment_cache(cached_hash, non_cached_hash) adapter_instance.fragment_cache(cached_hash, non_cached_hash)
end end
# rubocop:enable Metrics/AbcSize
def cache_key(adapter_instance) def cache_key(adapter_instance)
return @cache_key if defined?(@cache_key) return @cache_key if defined?(@cache_key)

View File

@ -1,59 +0,0 @@
module ActiveModel
class Serializer
module Configuration
include ActiveSupport::Configurable
extend ActiveSupport::Concern
# Configuration options may also be set in
# Serializers and Adapters
included do |base|
config = base.config
config.collection_serializer = ActiveModel::Serializer::CollectionSerializer
config.serializer_lookup_enabled = true
def config.array_serializer=(collection_serializer)
self.collection_serializer = collection_serializer
end
def config.array_serializer
collection_serializer
end
config.default_includes = '*'
config.adapter = :attributes
config.key_transform = nil
config.jsonapi_pagination_links_enabled = true
config.jsonapi_resource_type = :plural
config.jsonapi_namespace_separator = '-'.freeze
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
# 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
end
end
end

View File

@ -1,35 +0,0 @@
module ActiveModel
class Serializer
module Links
extend ActiveSupport::Concern
included do
with_options instance_writer: false, instance_reader: true do |serializer|
serializer.class_attribute :_links # @api private
self._links ||= {}
end
extend ActiveSupport::Autoload
end
module ClassMethods
def inherited(base)
super
base._links = _links.dup
end
# Define a link on a serializer.
# @example
# link(:self) { resource_url(object) }
# @example
# link(:self) { "http://example.com/resource/#{object.id}" }
# @example
# link :resource, "http://example.com/resource"
#
def link(name, value = nil, &block)
_links[name] = block || value
end
end
end
end
end

View File

@ -1,29 +0,0 @@
module ActiveModel
class Serializer
module Meta
extend ActiveSupport::Concern
included do
with_options instance_writer: false, instance_reader: true do |serializer|
serializer.class_attribute :_meta # @api private
end
extend ActiveSupport::Autoload
end
module ClassMethods
# 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 meta(value = nil, &block)
self._meta = block || value
end
end
end
end
end

View File

@ -1,25 +0,0 @@
module ActiveModel
class Serializer
module Type
extend ActiveSupport::Concern
included do
with_options instance_writer: false, instance_reader: true do |serializer|
serializer.class_attribute :_type # @api private
end
extend ActiveSupport::Autoload
end
module ClassMethods
# Set the JSON API type of a serializer.
# @example
# class AdminAuthorSerializer < ActiveModel::Serializer
# type 'authors'
def type(type)
self._type = type && type.to_s
end
end
end
end
end

View File

@ -1,7 +1,10 @@
module ActiveModel module ActiveModel
class Serializer class Serializer
# @api private # @api private
class HasManyReflection < CollectionReflection class HasManyReflection < Reflection
def collection?
true
end
end end
end end
end end

View File

@ -1,7 +1,7 @@
module ActiveModel module ActiveModel
class Serializer class Serializer
# @api private # @api private
class HasOneReflection < SingularReflection class HasOneReflection < Reflection
end end
end end
end end

View File

@ -0,0 +1,95 @@
module ActiveModel
class Serializer
# @api private
LazyAssociation = Struct.new(:reflection, :association_options) do
REFLECTION_OPTIONS = %i(key links polymorphic meta serializer virtual_value namespace).freeze
delegate :collection?, to: :reflection
def reflection_options
@reflection_options ||= reflection.options.dup.reject { |k, _| !REFLECTION_OPTIONS.include?(k) }
end
def object
@object ||= reflection.value(
association_options.fetch(:parent_serializer),
association_options.fetch(:include_slice)
)
end
alias_method :eval_reflection_block, :object
def include_data?
eval_reflection_block if reflection.block
reflection.include_data?(
association_options.fetch(:include_slice)
)
end
# @return [ActiveModel::Serializer, nil]
def serializer
return @serializer if defined?(@serializer)
if serializer_class
serialize_object!(object)
elsif !object.nil? && !object.instance_of?(Object)
cached_result[:virtual_value] = object
end
@serializer = cached_result[:serializer]
end
def virtual_value
cached_result[:virtual_value] || reflection_options[:virtual_value]
end
def serializer_class
return @serializer_class if defined?(@serializer_class)
serializer_for_options = { namespace: namespace }
serializer_for_options[:serializer] = reflection_options[:serializer] if reflection_options.key?(:serializer)
@serializer_class = association_options.fetch(:parent_serializer).class.serializer_for(object, serializer_for_options)
end
private
def cached_result
@cached_result ||= {}
end
def serialize_object!(object)
if collection?
if (serializer = instantiate_collection_serializer(object)).nil?
# BUG: per #2027, JSON API resource relationships are only id and type, and hence either
# *require* a serializer or we need to be a little clever about figuring out the id/type.
# In either case, returning the raw virtual value will almost always be incorrect.
#
# Should be reflection_options[:virtual_value] or adapter needs to figure out what to do
# with an object that is non-nil and has no defined serializer.
cached_result[:virtual_value] = object.try(:as_json) || object
else
cached_result[:serializer] = serializer
end
else
cached_result[:serializer] = instantiate_serializer(object)
end
end
def instantiate_serializer(object)
serializer_options = association_options.fetch(:parent_serializer_options).except(:serializer)
serializer_options[:serializer_context_class] = association_options.fetch(:parent_serializer).class
serializer = reflection_options.fetch(:serializer, nil)
serializer_options[:serializer] = serializer if serializer
serializer_class.new(object, serializer_options)
end
def instantiate_collection_serializer(object)
serializer = catch(:no_serializer) do
instantiate_serializer(object)
end
serializer
end
def namespace
reflection_options[:namespace] ||
association_options.fetch(:parent_serializer_options)[:namespace]
end
end
end
end

View File

@ -1,4 +1,5 @@
require 'active_model/serializer/field' require 'active_model/serializer/field'
require 'active_model/serializer/association'
module ActiveModel module ActiveModel
class Serializer class Serializer
@ -8,12 +9,26 @@ module ActiveModel
# @example # @example
# class PostSerializer < ActiveModel::Serializer # class PostSerializer < ActiveModel::Serializer
# has_one :author, serializer: AuthorSerializer # has_one :author, serializer: AuthorSerializer
# belongs_to :boss, type: :users, foreign_key: :boss_id
# has_many :comments # has_many :comments
# has_many :comments, key: :last_comments do # has_many :comments, key: :last_comments do
# object.comments.last(1) # object.comments.last(1)
# end # end
# has_many :secret_meta_data, if: :is_admin? # has_many :secret_meta_data, if: :is_admin?
# #
# has_one :blog do |serializer|
# meta count: object.roles.count
# serializer.cached_blog
# end
#
# private
#
# def cached_blog
# cache_store.fetch("cached_blog:#{object.updated_at}") do
# Blog.find(object.blog_id)
# end
# end
#
# def is_admin? # def is_admin?
# current_user.admin? # current_user.admin?
# end # end
@ -23,52 +38,118 @@ module ActiveModel
# 1) as 'comments' and named 'comments'. # 1) as 'comments' and named 'comments'.
# 2) as 'object.comments.last(1)' and named 'last_comments'. # 2) as 'object.comments.last(1)' and named 'last_comments'.
# #
# PostSerializer._reflections #=> # PostSerializer._reflections # =>
# # [ # # {
# # HasOneReflection.new(:author, serializer: AuthorSerializer), # # author: HasOneReflection.new(:author, serializer: AuthorSerializer),
# # HasManyReflection.new(:comments) # # comments: HasManyReflection.new(:comments)
# # HasManyReflection.new(:comments, { key: :last_comments }, #<Block>) # # last_comments: HasManyReflection.new(:comments, { key: :last_comments }, #<Block>)
# # HasManyReflection.new(:secret_meta_data, { if: :is_admin? }) # # secret_meta_data: HasManyReflection.new(:secret_meta_data, { if: :is_admin? })
# # ] # # }
# #
# So you can inspect reflections in your Adapters. # So you can inspect reflections in your Adapters.
#
class Reflection < Field class Reflection < Field
attr_reader :foreign_key, :type
def initialize(*) def initialize(*)
super super
@_links = {} options[:links] = {}
@_include_data = Serializer.config.include_data_default options[:include_data_setting] = Serializer.config.include_data_default
@_meta = nil options[:meta] = nil
@type = options.fetch(:type) do
class_name = options.fetch(:class_name, name.to_s.camelize.singularize)
class_name.underscore.pluralize.to_sym
end
@foreign_key = options.fetch(:foreign_key) do
if collection?
"#{name.to_s.singularize}_ids".to_sym
else
"#{name}_id".to_sym
end
end
end end
def link(name, value = nil, &block) # @api public
@_links[name] = block || value # @example
# has_one :blog do
# include_data false
# link :self, 'a link'
# link :related, 'another link'
# link :self, '//example.com/link_author/relationships/bio'
# id = object.profile.id
# link :related do
# "//example.com/profiles/#{id}" if id != 123
# end
# link :related do
# ids = object.likes.map(&:id).join(',')
# href "//example.com/likes/#{ids}"
# meta ids: ids
# end
# end
def link(name, value = nil)
options[:links][name] = block_given? ? Proc.new : value
:nil :nil
end end
def meta(value = nil, &block) # @api public
@_meta = block || value # @example
# has_one :blog do
# include_data false
# meta(id: object.blog.id)
# meta liked: object.likes.any?
# link :self do
# href object.blog.id.to_s
# meta(id: object.blog.id)
# end
def meta(value = nil)
options[:meta] = block_given? ? Proc.new : value
:nil :nil
end end
# @api public
# @example
# has_one :blog do
# include_data false
# link :self, 'a link'
# link :related, 'another link'
# end
#
# has_one :blog do
# include_data false
# link :self, 'a link'
# link :related, 'another link'
# end
#
# belongs_to :reviewer do
# meta name: 'Dan Brown'
# include_data true
# end
#
# has_many :tags, serializer: TagSerializer do
# link :self, '//example.com/link_author/relationships/tags'
# include_data :if_sideloaded
# end
def include_data(value = true) def include_data(value = true)
@_include_data = value options[:include_data_setting] = value
:nil :nil
end end
def collection?
false
end
def include_data?(include_slice)
include_data_setting = options[:include_data_setting]
case include_data_setting
when :if_sideloaded then include_slice.key?(name)
when true then true
when false then false
else fail ArgumentError, "Unknown include_data_setting '#{include_data_setting.inspect}'"
end
end
# @param serializer [ActiveModel::Serializer] # @param serializer [ActiveModel::Serializer]
# @yield [ActiveModel::Serializer] # @yield [ActiveModel::Serializer]
# @return [:nil, associated resource or resource collection] # @return [:nil, associated resource or resource collection]
# @example
# has_one :blog do |serializer|
# serializer.cached_blog
# end
#
# def cached_blog
# cache_store.fetch("cached_blog:#{object.updated_at}") do
# Blog.find(object.blog_id)
# end
# end
def value(serializer, include_slice) def value(serializer, include_slice)
@object = serializer.object @object = serializer.object
@scope = serializer.scope @scope = serializer.scope
@ -83,6 +164,11 @@ module ActiveModel
end end
end end
# @api private
def foreign_key_on
:related
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.
# #
@ -103,61 +189,19 @@ module ActiveModel
# comments_reflection.build_association(post_serializer, foo: 'bar') # comments_reflection.build_association(post_serializer, foo: 'bar')
# #
# @api private # @api private
#
def build_association(parent_serializer, parent_serializer_options, include_slice = {}) def build_association(parent_serializer, parent_serializer_options, include_slice = {})
reflection_options = options.dup association_options = {
parent_serializer: parent_serializer,
# Pass the parent's namespace onto the child serializer parent_serializer_options: parent_serializer_options,
reflection_options[:namespace] ||= parent_serializer_options[:namespace] include_slice: include_slice
}
association_value = value(parent_serializer, include_slice) Association.new(self, association_options)
serializer_class = parent_serializer.class.serializer_for(association_value, reflection_options)
reflection_options[:include_data] = include_data?(include_slice)
reflection_options[:links] = @_links
reflection_options[:meta] = @_meta
if serializer_class
serializer = catch(:no_serializer) do
serializer_class.new(
association_value,
serializer_options(parent_serializer, parent_serializer_options, reflection_options)
)
end
if serializer.nil?
reflection_options[:virtual_value] = association_value.try(:as_json) || association_value
else
reflection_options[:serializer] = serializer
end
elsif !association_value.nil? && !association_value.instance_of?(Object)
reflection_options[:virtual_value] = association_value
end
block = nil
Association.new(name, reflection_options, block)
end end
protected protected
# used in instance exec
attr_accessor :object, :scope attr_accessor :object, :scope
private
def include_data?(include_slice)
if @_include_data == :if_sideloaded
include_slice.key?(name)
else
@_include_data
end
end
def serializer_options(parent_serializer, 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[:serializer_context_class] = parent_serializer.class
serializer_options
end
end end
end end
end end

View File

@ -1,7 +0,0 @@
module ActiveModel
class Serializer
# @api private
class SingularReflection < Reflection
end
end
end

View File

@ -1,5 +1,5 @@
module ActiveModel module ActiveModel
class Serializer class Serializer
VERSION = '0.10.5'.freeze VERSION = '0.10.6'.freeze
end end
end end

View File

@ -257,7 +257,8 @@ module ActiveModelSerializers
def process_relationships(serializer, include_slice) def process_relationships(serializer, include_slice)
serializer.associations(include_slice).each do |association| serializer.associations(include_slice).each do |association|
process_relationship(association.serializer, include_slice[association.key]) # TODO(BF): Process relationship without evaluating lazy_association
process_relationship(association.lazy_association.serializer, include_slice[association.key])
end end
end end
@ -294,20 +295,8 @@ module ActiveModelSerializers
# {http://jsonapi.org/format/#document-resource-objects Document Resource Objects} # {http://jsonapi.org/format/#document-resource-objects Document Resource Objects}
def resource_object_for(serializer, include_slice = {}) def resource_object_for(serializer, include_slice = {})
resource_object = serializer.fetch(self) do resource_object = data_for(serializer, include_slice)
resource_object = ResourceIdentifier.new(serializer, instance_options).as_json
requested_fields = fieldset && fieldset.fields_for(resource_object[:type])
attributes = attributes_for(serializer, requested_fields)
resource_object[:attributes] = attributes if attributes.any?
resource_object
end
requested_associations = fieldset.fields_for(resource_object[:type]) || '*'
relationships = relationships_for(serializer, requested_associations, include_slice)
resource_object[:relationships] = relationships if relationships.any?
links = links_for(serializer)
# toplevel_links # toplevel_links
# definition: # definition:
# allOf # allOf
@ -321,7 +310,10 @@ module ActiveModelSerializers
# prs: # prs:
# https://github.com/rails-api/active_model_serializers/pull/1247 # https://github.com/rails-api/active_model_serializers/pull/1247
# https://github.com/rails-api/active_model_serializers/pull/1018 # https://github.com/rails-api/active_model_serializers/pull/1018
resource_object[:links] = links if links.any? if (links = links_for(serializer)).any?
resource_object ||= {}
resource_object[:links] = links
end
# toplevel_meta # toplevel_meta
# alias meta # alias meta
@ -331,12 +323,33 @@ module ActiveModelSerializers
# { # {
# :'git-ref' => 'abc123' # :'git-ref' => 'abc123'
# } # }
meta = meta_for(serializer) if (meta = meta_for(serializer)).present?
resource_object[:meta] = meta unless meta.blank? resource_object ||= {}
resource_object[:meta] = meta
end
resource_object resource_object
end end
def data_for(serializer, include_slice)
data = serializer.fetch(self) do
resource_object = ResourceIdentifier.new(serializer, instance_options).as_json
break nil if resource_object.nil?
requested_fields = fieldset && fieldset.fields_for(resource_object[:type])
attributes = attributes_for(serializer, requested_fields)
resource_object[:attributes] = attributes if attributes.any?
resource_object
end
data.tap do |resource_object|
next if resource_object.nil?
# NOTE(BF): the attributes are cached above, separately from the relationships, below.
requested_associations = fieldset.fields_for(resource_object[:type]) || '*'
relationships = relationships_for(serializer, requested_associations, include_slice)
resource_object[:relationships] = relationships if relationships.any?
end
end
# {http://jsonapi.org/format/#document-resource-object-relationships Document Resource Object Relationship} # {http://jsonapi.org/format/#document-resource-object-relationships Document Resource Object Relationship}
# relationships # relationships
# definition: # definition:

View File

@ -15,9 +15,7 @@ module ActiveModelSerializers
def as_json def as_json
hash = {} hash = {}
if association.options[:include_data] hash[:data] = data_for(association) if association.include_data?
hash[:data] = data_for(association)
end
links = links_for(association) links = links_for(association)
hash[:links] = links if links.any? hash[:links] = links if links.any?
@ -35,14 +33,45 @@ module ActiveModelSerializers
private private
# TODO(BF): Avoid db hit on belong_to_ releationship by using foreign_key on self
def data_for(association) def data_for(association)
serializer = association.serializer if association.collection?
if serializer.respond_to?(:each) data_for_many(association)
serializer.map { |s| ResourceIdentifier.new(s, serializable_resource_options).as_json } else
elsif (virtual_value = association.options[:virtual_value]) data_for_one(association)
end
end
def data_for_one(association)
if association.belongs_to? &&
parent_serializer.object.respond_to?(association.reflection.foreign_key)
id = parent_serializer.object.send(association.reflection.foreign_key)
type = association.reflection.type.to_s
ResourceIdentifier.for_type_with_id(type, id, serializable_resource_options)
else
# TODO(BF): Process relationship without evaluating lazy_association
serializer = association.lazy_association.serializer
if (virtual_value = association.virtual_value)
virtual_value
elsif serializer && association.object
ResourceIdentifier.new(serializer, serializable_resource_options).as_json
else
nil
end
end
end
def data_for_many(association)
# TODO(BF): Process relationship without evaluating lazy_association
collection_serializer = association.lazy_association.serializer
if collection_serializer.respond_to?(:each)
collection_serializer.map do |serializer|
ResourceIdentifier.new(serializer, serializable_resource_options).as_json
end
elsif (virtual_value = association.virtual_value)
virtual_value virtual_value
elsif serializer && serializer.object else
ResourceIdentifier.new(serializer, serializable_resource_options).as_json []
end end
end end

View File

@ -22,6 +22,14 @@ module ActiveModelSerializers
JsonApi.send(:transform_key_casing!, raw_type, transform_options) JsonApi.send(:transform_key_casing!, raw_type, transform_options)
end end
def self.for_type_with_id(type, id, options)
return nil if id.blank?
{
id: id.to_s,
type: type_for(:no_class_needed, type, options)
}
end
# {http://jsonapi.org/format/#document-resource-identifier-objects Resource Identifier Objects} # {http://jsonapi.org/format/#document-resource-identifier-objects Resource Identifier Objects}
def initialize(serializer, options) def initialize(serializer, options)
@id = id_for(serializer) @id = id_for(serializer)
@ -29,6 +37,7 @@ module ActiveModelSerializers
end end
def as_json def as_json
return nil if id.blank?
{ id: id, type: type } { id: id, type: type }
end end

View File

@ -1,12 +1,13 @@
# ActiveModelSerializers::Model is a convenient superclass for making your models # ActiveModelSerializers::Model is a convenient superclass for making your models
# from Plain-Old Ruby Objects (PORO). It also serves as a reference implementation # from Plain-Old Ruby Objects (PORO). It also serves as a reference implementation
# that satisfies ActiveModel::Serializer::Lint::Tests. # that satisfies ActiveModel::Serializer::Lint::Tests.
require 'active_support/core_ext/hash'
module ActiveModelSerializers module ActiveModelSerializers
class Model class Model
include ActiveModel::Serializers::JSON include ActiveModel::Serializers::JSON
include ActiveModel::Model include ActiveModel::Model
# Declare names of attributes to be included in +sttributes+ hash. # Declare names of attributes to be included in +attributes+ hash.
# Is only available as a class-method since the ActiveModel::Serialization mixin in Rails # Is only available as a class-method since the ActiveModel::Serialization mixin in Rails
# uses an +attribute_names+ local variable, which may conflict if we were to add instance methods here. # uses an +attribute_names+ local variable, which may conflict if we were to add instance methods here.
# #
@ -19,8 +20,8 @@ module ActiveModelSerializers
# Easily declare instance attributes with setters and getters for each. # Easily declare instance attributes with setters and getters for each.
# #
# All attributes to initialize an instance must have setters. # To initialize an instance, all attributes must have setters.
# However, the hash turned by +attributes+ instance method will ALWAYS # However, the hash returned by +attributes+ instance method will ALWAYS
# be the value of the initial attributes, regardless of what accessors are defined. # be the value of the initial attributes, regardless of what accessors are defined.
# The only way to change the change the attributes after initialization is # The only way to change the change the attributes after initialization is
# to mutate the +attributes+ directly. # to mutate the +attributes+ directly.
@ -58,7 +59,7 @@ module ActiveModelSerializers
# Override the +attributes+ method so that the hash is derived from +attribute_names+. # Override the +attributes+ method so that the hash is derived from +attribute_names+.
# #
# The the fields in +attribute_names+ determines the returned hash. # The fields in +attribute_names+ determines the returned hash.
# +attributes+ are returned frozen to prevent any expectations that mutation affects # +attributes+ are returned frozen to prevent any expectations that mutation affects
# the actual values in the model. # the actual values in the model.
def attributes def attributes

53
lib/tasks/rubocop.rake Normal file
View File

@ -0,0 +1,53 @@
begin
require 'rubocop'
require 'rubocop/rake_task'
rescue LoadError # rubocop:disable Lint/HandleExceptions
else
require 'rbconfig'
# https://github.com/bundler/bundler/blob/1b3eb2465a/lib/bundler/constants.rb#L2
windows_platforms = /(msdos|mswin|djgpp|mingw)/
if RbConfig::CONFIG['host_os'] =~ windows_platforms
desc 'No-op rubocop on Windows-- unsupported platform'
task :rubocop do
puts 'Skipping rubocop on Windows'
end
elsif defined?(::Rubinius)
desc 'No-op rubocop to avoid rbx segfault'
task :rubocop do
puts 'Skipping rubocop on rbx due to segfault'
puts 'https://github.com/rubinius/rubinius/issues/3499'
end
else
Rake::Task[:rubocop].clear if Rake::Task.task_defined?(:rubocop)
patterns = [
'Gemfile',
'Rakefile',
'lib/**/*.{rb,rake}',
'config/**/*.rb',
'app/**/*.rb',
'test/**/*.rb'
]
desc 'Execute rubocop'
RuboCop::RakeTask.new(:rubocop) do |task|
task.options = ['--rails', '--display-cop-names', '--display-style-guide']
task.formatters = ['progress']
task.patterns = patterns
task.fail_on_error = true
end
namespace :rubocop do
desc 'Auto-gen rubocop config'
task :auto_gen_config do
options = ['--auto-gen-config'].concat patterns
require 'benchmark'
result = 0
cli = RuboCop::CLI.new
time = Benchmark.realtime do
result = cli.run(options)
end
puts "Finished in #{time} seconds" if cli.options[:debug]
abort('RuboCop failed!') if result.nonzero?
end
end
end
end

View File

@ -19,7 +19,7 @@ module ActionController
end end
def render_using_adapter_override def render_using_adapter_override
@profile = Profile.new(name: 'Name 1', description: 'Description 1', comments: 'Comments 1') @profile = Profile.new(id: 'render_using_adapter_override', name: 'Name 1', description: 'Description 1', comments: 'Comments 1')
render json: @profile, adapter: :json_api render json: @profile, adapter: :json_api
end end
@ -41,7 +41,7 @@ module ActionController
expected = { expected = {
data: { data: {
id: @controller.instance_variable_get(:@profile).id.to_s, id: 'render_using_adapter_override',
type: 'profiles', type: 'profiles',
attributes: { attributes: {
name: 'Name 1', name: 'Name 1',

View File

@ -30,18 +30,17 @@ module ActiveModel
def test_has_many_and_has_one def test_has_many_and_has_one
@author_serializer.associations.each do |association| @author_serializer.associations.each do |association|
key = association.key key = association.key
serializer = association.serializer serializer = association.lazy_association.serializer
options = association.options
case key case key
when :posts when :posts
assert_equal true, options.fetch(:include_data) assert_equal true, association.include_data?
assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer) assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer)
when :bio when :bio
assert_equal true, options.fetch(:include_data) assert_equal true, association.include_data?
assert_nil serializer assert_nil serializer
when :roles when :roles
assert_equal true, options.fetch(:include_data) assert_equal true, association.include_data?
assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer) assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer)
else else
flunk "Unknown association: #{key}" flunk "Unknown association: #{key}"
@ -56,12 +55,11 @@ module ActiveModel
end end
post_serializer_class.new(@post).associations.each do |association| post_serializer_class.new(@post).associations.each do |association|
key = association.key key = association.key
serializer = association.serializer serializer = association.lazy_association.serializer
options = association.options
assert_equal :tags, key assert_equal :tags, key
assert_nil serializer assert_nil serializer
assert_equal [{ id: 'tagid', name: '#hashtagged' }].to_json, options[:virtual_value].to_json assert_equal [{ id: 'tagid', name: '#hashtagged' }].to_json, association.virtual_value.to_json
end end
end end
@ -70,7 +68,7 @@ module ActiveModel
.associations .associations
.detect { |assoc| assoc.key == :comments } .detect { |assoc| assoc.key == :comments }
comment_serializer = association.serializer.first comment_serializer = association.lazy_association.serializer.first
class << comment_serializer class << comment_serializer
def custom_options def custom_options
instance_options instance_options
@ -82,7 +80,7 @@ module ActiveModel
def test_belongs_to def test_belongs_to
@comment_serializer.associations.each do |association| @comment_serializer.associations.each do |association|
key = association.key key = association.key
serializer = association.serializer serializer = association.lazy_association.serializer
case key case key
when :post when :post
@ -93,7 +91,7 @@ module ActiveModel
flunk "Unknown association: #{key}" flunk "Unknown association: #{key}"
end end
assert_equal true, association.options.fetch(:include_data) assert_equal true, association.include_data?
end end
end end
@ -139,6 +137,34 @@ module ActiveModel
assert expected_association_keys.include? :site assert expected_association_keys.include? :site
end end
class BelongsToBlogModel < ::Model
attributes :id, :title
associations :blog
end
class BelongsToBlogModelSerializer < ActiveModel::Serializer
type :posts
belongs_to :blog
end
def test_belongs_to_doesnt_load_record
attributes = { id: 1, title: 'Belongs to Blog', blog: Blog.new(id: 5) }
post = BelongsToBlogModel.new(attributes)
class << post
def blog
fail 'should use blog_id'
end
def blog_id
5
end
end
actual = serializable(post, adapter: :json_api, serializer: BelongsToBlogModelSerializer).as_json
expected = { data: { id: '1', type: 'posts', relationships: { blog: { data: { id: '5', type: 'blogs' } } } } }
assert_equal expected, actual
end
class InlineAssociationTestPostSerializer < ActiveModel::Serializer class InlineAssociationTestPostSerializer < ActiveModel::Serializer
has_many :comments has_many :comments
has_many :comments, key: :last_comments do has_many :comments, key: :last_comments do
@ -203,11 +229,11 @@ module ActiveModel
@post_serializer.associations.each do |association| @post_serializer.associations.each do |association|
case association.key case association.key
when :comments when :comments
assert_instance_of(ResourceNamespace::CommentSerializer, association.serializer.first) assert_instance_of(ResourceNamespace::CommentSerializer, association.lazy_association.serializer.first)
when :author when :author
assert_instance_of(ResourceNamespace::AuthorSerializer, association.serializer) assert_instance_of(ResourceNamespace::AuthorSerializer, association.lazy_association.serializer)
when :description when :description
assert_instance_of(ResourceNamespace::DescriptionSerializer, association.serializer) assert_instance_of(ResourceNamespace::DescriptionSerializer, association.lazy_association.serializer)
else else
flunk "Unknown association: #{key}" flunk "Unknown association: #{key}"
end end
@ -245,11 +271,11 @@ module ActiveModel
@post_serializer.associations.each do |association| @post_serializer.associations.each do |association|
case association.key case association.key
when :comments when :comments
assert_instance_of(PostSerializer::CommentSerializer, association.serializer.first) assert_instance_of(PostSerializer::CommentSerializer, association.lazy_association.serializer.first)
when :author when :author
assert_instance_of(PostSerializer::AuthorSerializer, association.serializer) assert_instance_of(PostSerializer::AuthorSerializer, association.lazy_association.serializer)
when :description when :description
assert_instance_of(PostSerializer::DescriptionSerializer, association.serializer) assert_instance_of(PostSerializer::DescriptionSerializer, association.lazy_association.serializer)
else else
flunk "Unknown association: #{key}" flunk "Unknown association: #{key}"
end end
@ -260,7 +286,7 @@ module ActiveModel
def test_conditional_associations def test_conditional_associations
model = Class.new(::Model) do model = Class.new(::Model) do
attributes :true, :false attributes :true, :false
associations :association associations :something
end.new(true: true, false: false) end.new(true: true, false: false)
scenarios = [ scenarios = [
@ -284,7 +310,7 @@ module ActiveModel
scenarios.each do |s| scenarios.each do |s|
serializer = Class.new(ActiveModel::Serializer) do serializer = Class.new(ActiveModel::Serializer) do
belongs_to :association, s[:options] belongs_to :something, s[:options]
def true def true
true true
@ -296,7 +322,7 @@ module ActiveModel
end end
hash = serializable(model, serializer: serializer).serializable_hash hash = serializable(model, serializer: serializer).serializable_hash
assert_equal(s[:included], hash.key?(:association), "Error with #{s[:options]}") assert_equal(s[:included], hash.key?(:something), "Error with #{s[:options]}")
end end
end end
@ -341,8 +367,8 @@ module ActiveModel
@author_serializer = AuthorSerializer.new(@author) @author_serializer = AuthorSerializer.new(@author)
@inherited_post_serializer = InheritedPostSerializer.new(@post) @inherited_post_serializer = InheritedPostSerializer.new(@post)
@inherited_author_serializer = InheritedAuthorSerializer.new(@author) @inherited_author_serializer = InheritedAuthorSerializer.new(@author)
@author_associations = @author_serializer.associations.to_a @author_associations = @author_serializer.associations.to_a.sort_by(&:name)
@inherited_author_associations = @inherited_author_serializer.associations.to_a @inherited_author_associations = @inherited_author_serializer.associations.to_a.sort_by(&:name)
@post_associations = @post_serializer.associations.to_a @post_associations = @post_serializer.associations.to_a
@inherited_post_associations = @inherited_post_serializer.associations.to_a @inherited_post_associations = @inherited_post_serializer.associations.to_a
end end
@ -361,28 +387,35 @@ module ActiveModel
test 'a serializer inheriting from another serializer can redefine has_many and has_one associations' do test 'a serializer inheriting from another serializer can redefine has_many and has_one associations' do
expected = [:roles, :bio].sort expected = [:roles, :bio].sort
result = (@inherited_author_associations - @author_associations).map(&:name).sort result = (@inherited_author_associations.map(&:reflection) - @author_associations.map(&:reflection)).map(&:name)
assert_equal(result, expected) assert_equal(result, expected)
assert_equal [true, false, true], @inherited_author_associations.map(&:polymorphic?)
assert_equal [false, false, false], @author_associations.map(&:polymorphic?)
end end
test 'a serializer inheriting from another serializer can redefine belongs_to associations' do test 'a serializer inheriting from another serializer can redefine belongs_to associations' do
assert_equal [:author, :comments, :blog], @post_associations.map(&:name) assert_equal [:author, :comments, :blog], @post_associations.map(&:name)
assert_equal [:author, :comments, :blog, :comments], @inherited_post_associations.map(&:name) assert_equal [:author, :comments, :blog, :comments], @inherited_post_associations.map(&:name)
refute @post_associations.detect { |assoc| assoc.name == :author }.options.key?(:polymorphic) refute @post_associations.detect { |assoc| assoc.name == :author }.polymorphic?
assert_equal true, @inherited_post_associations.detect { |assoc| assoc.name == :author }.options.fetch(:polymorphic) assert @inherited_post_associations.detect { |assoc| assoc.name == :author }.polymorphic?
refute @post_associations.detect { |assoc| assoc.name == :comments }.options.key?(:key) refute @post_associations.detect { |assoc| assoc.name == :comments }.key?
original_comment_assoc, new_comments_assoc = @inherited_post_associations.select { |assoc| assoc.name == :comments } original_comment_assoc, new_comments_assoc = @inherited_post_associations.select { |assoc| assoc.name == :comments }
refute original_comment_assoc.options.key?(:key) refute original_comment_assoc.key?
assert_equal :reviews, new_comments_assoc.options.fetch(:key) assert_equal :reviews, new_comments_assoc.key
assert_equal @post_associations.detect { |assoc| assoc.name == :blog }, @inherited_post_associations.detect { |assoc| assoc.name == :blog } original_blog = @post_associations.detect { |assoc| assoc.name == :blog }
inherited_blog = @inherited_post_associations.detect { |assoc| assoc.name == :blog }
original_parent_serializer = original_blog.lazy_association.association_options.delete(:parent_serializer)
inherited_parent_serializer = inherited_blog.lazy_association.association_options.delete(:parent_serializer)
assert_equal PostSerializer, original_parent_serializer.class
assert_equal InheritedPostSerializer, inherited_parent_serializer.class
end end
test 'a serializer inheriting from another serializer can have an additional association with the same name but with different key' do test 'a serializer inheriting from another serializer can have an additional association with the same name but with different key' do
expected = [:author, :comments, :blog, :reviews].sort expected = [:author, :comments, :blog, :reviews].sort
result = @inherited_post_serializer.associations.map { |a| a.options.fetch(:key, a.name) }.sort result = @inherited_post_serializer.associations.map(&:key).sort
assert_equal(result, expected) assert_equal(result, expected)
end end
end end

View File

@ -0,0 +1,427 @@
require 'test_helper'
module ActiveModel
class Serializer
class ReflectionTest < ActiveSupport::TestCase
class Blog < ActiveModelSerializers::Model
attributes :id
end
class BlogSerializer < ActiveModel::Serializer
type 'blog'
attributes :id
end
setup do
@expected_meta = { id: 1 }
@expected_links = { self: 'no_uri_validation' }
@empty_links = {}
model_attributes = { blog: Blog.new(@expected_meta) }
@model = Class.new(ActiveModelSerializers::Model) do
attributes(*model_attributes.keys)
def self.name
'TestModel'
end
end.new(model_attributes)
@instance_options = {}
end
def evaluate_association_value(association)
association.lazy_association.eval_reflection_block
end
# TODO: Remaining tests
# test_reflection_value_block_with_scope
# test_reflection_value_uses_serializer_instance_method
# test_reflection_excluded_eh_blank_is_false
# test_reflection_excluded_eh_if
# test_reflection_excluded_eh_unless
# test_evaluate_condition_symbol_serializer_method
# test_evaluate_condition_string_serializer_method
# test_evaluate_condition_proc
# test_evaluate_condition_proc_yields_serializer
# test_evaluate_condition_other
# test_options_key
# test_options_polymorphic
# test_options_serializer
# test_options_virtual_value
# test_options_namespace
def test_reflection_value
serializer_class = Class.new(ActiveModel::Serializer) do
has_one :blog
end
serializer_instance = serializer_class.new(@model, @instance_options)
# Get Reflection
reflection = serializer_class._reflections.fetch(:blog)
# Assert
assert_nil reflection.block
assert_equal Serializer.config.include_data_default, reflection.options.fetch(:include_data_setting)
assert_equal true, reflection.options.fetch(:include_data_setting)
include_slice = :does_not_matter
assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice)
end
def test_reflection_value_block
serializer_class = Class.new(ActiveModel::Serializer) do
has_one :blog do
object.blog
end
end
serializer_instance = serializer_class.new(@model, @instance_options)
# Get Reflection
reflection = serializer_class._reflections.fetch(:blog)
# Assert
assert_respond_to reflection.block, :call
assert_equal Serializer.config.include_data_default, reflection.options.fetch(:include_data_setting)
assert_equal true, reflection.options.fetch(:include_data_setting)
include_slice = :does_not_matter
assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice)
end
def test_reflection_value_block_with_explicit_include_data_true
serializer_class = Class.new(ActiveModel::Serializer) do
has_one :blog do
include_data true
object.blog
end
end
serializer_instance = serializer_class.new(@model, @instance_options)
# Get Reflection
reflection = serializer_class._reflections.fetch(:blog)
# Assert
assert_respond_to reflection.block, :call
assert_equal Serializer.config.include_data_default, reflection.options.fetch(:include_data_setting)
assert_equal true, reflection.options.fetch(:include_data_setting)
include_slice = :does_not_matter
assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice)
end
def test_reflection_value_block_with_include_data_false_mutates_the_reflection_include_data
serializer_class = Class.new(ActiveModel::Serializer) do
has_one :blog do
include_data false
object.blog
end
end
serializer_instance = serializer_class.new(@model, @instance_options)
# Get Reflection
reflection = serializer_class._reflections.fetch(:blog)
# Assert
assert_respond_to reflection.block, :call
assert_equal true, reflection.options.fetch(:include_data_setting)
include_slice = :does_not_matter
assert_nil reflection.send(:value, serializer_instance, include_slice)
assert_equal false, reflection.options.fetch(:include_data_setting)
end
def test_reflection_value_block_with_include_data_if_sideloaded_included_mutates_the_reflection_include_data
serializer_class = Class.new(ActiveModel::Serializer) do
has_one :blog do
include_data :if_sideloaded
object.blog
end
end
serializer_instance = serializer_class.new(@model, @instance_options)
# Get Reflection
reflection = serializer_class._reflections.fetch(:blog)
# Assert
assert_respond_to reflection.block, :call
assert_equal true, reflection.options.fetch(:include_data_setting)
include_slice = {}
assert_nil reflection.send(:value, serializer_instance, include_slice)
assert_equal :if_sideloaded, reflection.options.fetch(:include_data_setting)
end
def test_reflection_value_block_with_include_data_if_sideloaded_excluded_mutates_the_reflection_include_data
serializer_class = Class.new(ActiveModel::Serializer) do
has_one :blog do
include_data :if_sideloaded
object.blog
end
end
serializer_instance = serializer_class.new(@model, @instance_options)
# Get Reflection
reflection = serializer_class._reflections.fetch(:blog)
# Assert
assert_respond_to reflection.block, :call
assert_equal true, reflection.options.fetch(:include_data_setting)
include_slice = { blog: :does_not_matter }
assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice)
assert_equal :if_sideloaded, reflection.options.fetch(:include_data_setting)
end
def test_reflection_block_with_link_mutates_the_reflection_links
serializer_class = Class.new(ActiveModel::Serializer) do
has_one :blog do
link :self, 'no_uri_validation'
end
end
serializer_instance = serializer_class.new(@model, @instance_options)
# Get Reflection
reflection = serializer_class._reflections.fetch(:blog)
assert_equal @empty_links, reflection.options.fetch(:links)
# Build Association
association = reflection.build_association(serializer_instance, @instance_options)
# Assert association links empty when not yet evaluated
assert_equal @empty_links, reflection.options.fetch(:links)
assert_equal @empty_links, association.links
evaluate_association_value(association)
assert_equal @expected_links, association.links
assert_equal @expected_links, reflection.options.fetch(:links)
end
def test_reflection_block_with_link_block_mutates_the_reflection_links
serializer_class = Class.new(ActiveModel::Serializer) do
has_one :blog do
link :self do
'no_uri_validation'
end
end
end
serializer_instance = serializer_class.new(@model, @instance_options)
# Get Reflection
reflection = serializer_class._reflections.fetch(:blog)
assert_equal @empty_links, reflection.options.fetch(:links)
# Build Association
association = reflection.build_association(serializer_instance, @instance_options)
# Assert association links empty when not yet evaluated
assert_equal @empty_links, association.links
evaluate_association_value(association)
# Assert before instance_eval link
link = association.links.fetch(:self)
assert_respond_to link, :call
assert_respond_to reflection.options.fetch(:links).fetch(:self), :call
# Assert after instance_eval link
assert_equal @expected_links.fetch(:self), reflection.instance_eval(&link)
assert_respond_to reflection.options.fetch(:links).fetch(:self), :call
end
def test_reflection_block_with_meta_mutates_the_reflection_meta
serializer_class = Class.new(ActiveModel::Serializer) do
has_one :blog do
meta(id: object.blog.id)
end
end
serializer_instance = serializer_class.new(@model, @instance_options)
# Get Reflection
reflection = serializer_class._reflections.fetch(:blog)
assert_nil reflection.options.fetch(:meta)
# Build Association
association = reflection.build_association(serializer_instance, @instance_options)
evaluate_association_value(association)
assert_equal @expected_meta, association.meta
assert_equal @expected_meta, reflection.options.fetch(:meta)
end
def test_reflection_block_with_meta_block_mutates_the_reflection_meta
serializer_class = Class.new(ActiveModel::Serializer) do
has_one :blog do
meta do
{ id: object.blog.id }
end
end
end
serializer_instance = serializer_class.new(@model, @instance_options)
# Get Reflection
reflection = serializer_class._reflections.fetch(:blog)
assert_nil reflection.options.fetch(:meta)
# Build Association
association = reflection.build_association(serializer_instance, @instance_options)
# Assert before instance_eval meta
evaluate_association_value(association)
assert_respond_to association.meta, :call
assert_respond_to reflection.options.fetch(:meta), :call
# Assert after instance_eval meta
assert_equal @expected_meta, reflection.instance_eval(&association.meta)
assert_respond_to reflection.options.fetch(:meta), :call
assert_respond_to association.meta, :call
end
# rubocop:disable Metrics/AbcSize
def test_reflection_block_with_meta_in_link_block_mutates_the_reflection_meta
serializer_class = Class.new(ActiveModel::Serializer) do
has_one :blog do
link :self do
meta(id: object.blog.id)
'no_uri_validation'
end
end
end
serializer_instance = serializer_class.new(@model, @instance_options)
# Get Reflection
reflection = serializer_class._reflections.fetch(:blog)
assert_nil reflection.options.fetch(:meta)
assert_equal @empty_links, reflection.options.fetch(:links)
# Build Association
association = reflection.build_association(serializer_instance, @instance_options)
# Assert before instance_eval link meta
assert_nil association.meta
assert_nil reflection.options.fetch(:meta)
evaluate_association_value(association)
link = association.links.fetch(:self)
assert_respond_to link, :call
assert_respond_to reflection.options.fetch(:links).fetch(:self), :call
assert_nil reflection.options.fetch(:meta)
# Assert after instance_eval link
assert_equal 'no_uri_validation', reflection.instance_eval(&link)
assert_equal @expected_meta, reflection.options.fetch(:meta)
assert_equal @expected_meta, association.meta
end
# rubocop:enable Metrics/AbcSize
# rubocop:disable Metrics/AbcSize
def test_reflection_block_with_meta_block_in_link_block_mutates_the_reflection_meta
serializer_class = Class.new(ActiveModel::Serializer) do
has_one :blog do
link :self do
meta do
{ id: object.blog.id }
end
'no_uri_validation'
end
end
end
serializer_instance = serializer_class.new(@model, @instance_options)
# Get Reflection
reflection = serializer_class._reflections.fetch(:blog)
assert_nil reflection.options.fetch(:meta)
# Build Association
association = reflection.build_association(serializer_instance, @instance_options)
assert_nil association.meta
assert_nil reflection.options.fetch(:meta)
# Assert before instance_eval link
evaluate_association_value(association)
link = association.links.fetch(:self)
assert_nil reflection.options.fetch(:meta)
assert_respond_to link, :call
assert_respond_to association.links.fetch(:self), :call
# Assert after instance_eval link
assert_equal 'no_uri_validation', reflection.instance_eval(&link)
assert_respond_to association.links.fetch(:self), :call
# Assert before instance_eval link meta
assert_respond_to reflection.options.fetch(:meta), :call
assert_respond_to association.meta, :call
# Assert after instance_eval link meta
assert_equal @expected_meta, reflection.instance_eval(&reflection.options.fetch(:meta))
assert_respond_to association.meta, :call
end
# rubocop:enable Metrics/AbcSize
def test_no_href_in_vanilla_reflection
serializer_class = Class.new(ActiveModel::Serializer) do
has_one :blog do
link :self do
href 'no_uri_validation'
end
end
end
serializer_instance = serializer_class.new(@model, @instance_options)
# Get Reflection
reflection = serializer_class._reflections.fetch(:blog)
assert_equal @empty_links, reflection.options.fetch(:links)
# Build Association
association = reflection.build_association(serializer_instance, @instance_options)
# Assert before instance_eval link
evaluate_association_value(association)
link = association.links.fetch(:self)
assert_respond_to link, :call
# Assert after instance_eval link
exception = assert_raise(NoMethodError) do
reflection.instance_eval(&link)
end
assert_match(/undefined method `href'/, exception.message)
end
# rubocop:disable Metrics/AbcSize
def test_mutating_reflection_block_is_not_thread_safe
serializer_class = Class.new(ActiveModel::Serializer) do
has_one :blog do
meta(id: object.blog.id)
end
end
model1_meta = @expected_meta
# Evaluate reflection meta for model with id 1
serializer_instance = serializer_class.new(@model, @instance_options)
reflection = serializer_class._reflections.fetch(:blog)
assert_nil reflection.options.fetch(:meta)
association = reflection.build_association(serializer_instance, @instance_options)
evaluate_association_value(association)
assert_equal model1_meta, association.meta
assert_equal model1_meta, reflection.options.fetch(:meta)
model2_meta = @expected_meta.merge(id: 2)
# Evaluate reflection meta for model with id 2
@model.blog.id = 2
assert_equal 2, @model.blog.id # sanity check
serializer_instance = serializer_class.new(@model, @instance_options)
reflection = serializer_class._reflections.fetch(:blog)
# WARN: Thread-safety issue
# Before the reflection is evaluated, it has the value from the previous evaluation
assert_equal model1_meta, reflection.options.fetch(:meta)
association = reflection.build_association(serializer_instance, @instance_options)
evaluate_association_value(association)
assert_equal model2_meta, association.meta
assert_equal model2_meta, reflection.options.fetch(:meta)
end
# rubocop:enable Metrics/AbcSize
end
end
end