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:
TargetRubyVersion: 2.1
Exclude:
- config/initializers/forbidden_yaml.rb
- !ruby/regexp /(vendor|bundle|bin|db|tmp)\/.*/
DisplayCopNames: 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:
Enabled: true

View File

@ -1,6 +1,6 @@
## 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:
@ -14,6 +14,20 @@ Fixes:
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)
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)
- [#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)
- [#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)

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.
- [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)
- [![API Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/gems/active_model_serializers/0.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.6)
- [Guides](docs)
- [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)

View File

@ -7,6 +7,7 @@ begin
require 'simplecov'
rescue LoadError # rubocop:disable Lint/HandleExceptions
end
import('lib/tasks/rubocop.rake')
Bundler::GemHelper.install_tasks
@ -30,36 +31,6 @@ namespace :yard do
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'
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 'simplecov', '~> 0.11'
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 'rake', ['>= 10.0', '< 12.0']
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
when the resource names are included in the `include` option.
Including nested associated resources is also supported.
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.
Example of the usage:
```ruby
render json: @posts, include: ['author', 'comments', 'comments.author']
# or
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:
- `*` 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
```
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:
@ -182,6 +184,20 @@ It could be combined, like above, with other paths in any combination desired.
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
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
attributes :name, :body
belongs_to :post_id
belongs_to :post
end
```

View File

@ -203,7 +203,7 @@ link(:link_name) { url_for(controller: 'controller_name', action: 'index', only_
#### include
PR please :)
See [Adapters: Include Option](/docs/general/adapters.md#include-option).
#### Overriding the root key
@ -260,15 +260,29 @@ Note that by using a string and symbol, Ruby will assume the namespace is define
#### 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
PR please :)
See [Serializers: Scope](/docs/general/serializers.md#scope).
#### scope_name
PR please :)
See [Serializers: Scope](/docs/general/serializers.md#scope).
## Using a serializer without `render`

View File

@ -64,6 +64,10 @@ Where:
- `unless:`
- `virtual_value:`
- `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.
- prevents `association_name` method from being called.
- 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
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
PR please :)
Returns the key used by the adapter as the resource root. See [root](#root) for more information.
## Examples

View File

@ -72,7 +72,7 @@ ActiveModelSerializers pagination relies on a paginated collection with the meth
### 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.

View File

@ -74,6 +74,9 @@ Then, in your controller you can tell rails you're accepting and rendering the j
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

View File

@ -4,13 +4,7 @@ require 'active_model/serializer/collection_serializer'
require 'active_model/serializer/array_serializer'
require 'active_model/serializer/error_serializer'
require 'active_model/serializer/errors_serializer'
require 'active_model/serializer/concerns/associations'
require 'active_model/serializer/concerns/attributes'
require 'active_model/serializer/concerns/caching'
require 'active_model/serializer/concerns/configuration'
require 'active_model/serializer/concerns/links'
require 'active_model/serializer/concerns/meta'
require 'active_model/serializer/concerns/type'
require 'active_model/serializer/fieldset'
require 'active_model/serializer/lint'
@ -18,33 +12,40 @@ require 'active_model/serializer/lint'
# reified when subclassed to decorate a resource.
module ActiveModel
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.
SERIALIZABLE_HASH_VALID_KEYS = [:only, :except, :methods, :include, :root].freeze
extend ActiveSupport::Autoload
autoload :Adapter
autoload :Null
include Configuration
include Associations
include Attributes
autoload :Attribute
autoload :Association
autoload :Reflection
autoload :SingularReflection
autoload :CollectionReflection
autoload :BelongsToReflection
autoload :HasOneReflection
autoload :HasManyReflection
include ActiveSupport::Configurable
include Caching
include Links
include Meta
include Type
# @param resource [ActiveRecord::Base, ActiveModelSerializers::Model]
# @return [ActiveModel::Serializer]
# Preferentially returns
# 1. resource.serializer
# 1. resource.serializer_class
# 2. ArraySerializer when resource is a collection
# 3. options[:serializer]
# 4. lookup serializer when resource is a Class
def self.serializer_for(resource, options = {})
if resource.respond_to?(:serializer_class)
resource.serializer_class
elsif resource.respond_to?(:to_ary)
def self.serializer_for(resource_or_class, options = {})
if resource_or_class.respond_to?(:serializer_class)
resource_or_class.serializer_class
elsif resource_or_class.respond_to?(:to_ary)
config.collection_serializer
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
@ -91,6 +92,8 @@ module ActiveModel
serializer_class
elsif klass.superclass
get_serializer_for(klass.superclass)
else
nil # No serializer found
end
end
end
@ -111,6 +114,193 @@ module ActiveModel
@serialization_adapter_instance ||= ActiveModelSerializers::Adapter::Attributes
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
# `scope_name` is set as :current_user by default in the controller.
@ -131,53 +321,49 @@ module ActiveModel
true
end
# Return the +attributes+ of +object+ as presented
# by the serializer.
def attributes(requested_attrs = nil, reload = false)
@attributes = nil if reload
@attributes ||= self.class._attributes_data.each_with_object({}) do |(key, attr), hash|
next if attr.excluded?(self)
next unless requested_attrs.nil? || requested_attrs.include?(key)
hash[key] = attr.value(self)
end
end
# @param [JSONAPI::IncludeDirective] include_directive (defaults to the
# +default_include_directive+ config value when not provided)
# @return [Enumerator<Association>]
def associations(include_directive = ActiveModelSerializers.default_include_directive, include_slice = nil)
include_slice ||= include_directive
return 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
# associations, similar to how ActiveModel::Serializers::JSON is used
# 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)
adapter_options ||= {}
options[:include_directive] ||= ActiveModel::Serializer.include_directive_from_options(adapter_options)
cached_attributes = adapter_options[:cached_attributes] ||= {}
resource = fetch_attributes(options[:fields], cached_attributes, adapter_instance)
relationships = resource_relationships(adapter_options, options, adapter_instance)
resource = attributes_hash(adapter_options, options, adapter_instance)
relationships = associations_hash(adapter_options, options, adapter_instance)
resource.merge(relationships)
end
alias to_hash serializable_hash
alias to_h 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)
serializable_hash(adapter_opts)
end
@ -196,32 +382,24 @@ module ActiveModel
end
# @api private
def resource_relationships(adapter_options, options, adapter_instance)
relationships = {}
include_directive = options.fetch(:include_directive)
associations(include_directive).each do |association|
adapter_opts = adapter_options.merge(include_directive: include_directive[association.key])
relationships[association.key] ||= relationship_value_for(association, adapter_opts, adapter_instance)
def attributes_hash(_adapter_options, options, adapter_instance)
if self.class.cache_enabled?
fetch_attributes(options[:fields], options[:cached_attributes] || {}, adapter_instance)
elsif self.class.fragment_cache_enabled?
fetch_attributes_fragment(adapter_instance, options[:cached_attributes] || {})
else
attributes(options[:fields], true)
end
relationships
end
# @api private
def relationship_value_for(association, adapter_options, adapter_instance)
return association.options[:virtual_value] if association.options[:virtual_value]
association_serializer = association.serializer
association_object = association_serializer && association_serializer.object
return unless association_object
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 }
def associations_hash(adapter_options, options, adapter_instance)
include_directive = options.fetch(:include_directive)
include_slice = options[:include_slice]
associations(include_directive, include_slice).each_with_object({}) do |association, relationships|
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)
end
relationship_value
end
protected

View File

@ -1,34 +1,71 @@
require 'active_model/serializer/lazy_association'
module ActiveModel
class Serializer
# This class holds all information about serializer's association.
#
# @attr [Symbol] name
# @attr [Hash{Symbol => Object}] options
# @attr [block]
#
# @example
# Association.new(:comments, { serializer: CommentSummarySerializer })
#
class Association < Field
# @return [Symbol]
def key
options.fetch(:key, name)
# @api private
Association = Struct.new(:reflection, :association_options) do
attr_reader :lazy_association
delegate :object, :include_data?, :virtual_value, :collection?, to: :lazy_association
def initialize(*)
super
@lazy_association = LazyAssociation.new(reflection, association_options)
end
# @return [ActiveModel::Serializer, nil]
def serializer
options[:serializer]
# @return [Symbol]
delegate :name, to: :reflection
# @return [Symbol]
def key
reflection_options.fetch(:key, name)
end
# @return [True,False]
def key?
reflection_options.key?(:key)
end
# @return [Hash]
def links
options.fetch(:links) || {}
reflection_options.fetch(:links) || {}
end
# @return [Hash, nil]
# This gets mutated, so cannot use the cached reflection_options
def meta
options[:meta]
reflection.options[:meta]
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

View File

@ -1,7 +1,11 @@
module ActiveModel
class Serializer
# @api private
class BelongsToReflection < SingularReflection
class BelongsToReflection < Reflection
# @api private
def foreign_key_on
:self
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
def inherited(base)
super
caller_line = caller[1]
base._cache_digest_file_path = caller_line
super
end
def _cache_digest
@ -68,6 +68,18 @@ module ActiveModel
_cache_options && _cache_options[:skip_digest]
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
cached = _cache_only ? _cache_only : _attributes - _cache_except
cached = cached.map! { |field| _attributes_keys.fetch(field, field) }
@ -158,6 +170,7 @@ module ActiveModel
# Read cache from cache_store
# @return [Hash]
# Used in CollectionSerializer to set :cached_attributes
def cache_read_multi(collection_serializer, adapter_instance, include_directive)
return {} if ActiveModelSerializers.config.cache_store.blank?
@ -180,12 +193,14 @@ module ActiveModel
cache_keys << object_cache_key(serializer, adapter_instance)
serializer.associations(include_directive).each do |association|
if association.serializer.respond_to?(:each)
association.serializer.each do |sub_serializer|
# TODO(BF): Process relationship without evaluating lazy_association
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)
end
else
cache_keys << object_cache_key(association.serializer, adapter_instance)
cache_keys << object_cache_key(association_serializer, adapter_instance)
end
end
end
@ -203,23 +218,18 @@ module ActiveModel
### INSTANCE METHODS
def fetch_attributes(fields, cached_attributes, adapter_instance)
if serializer_class.cache_enabled?
key = cache_key(adapter_instance)
cached_attributes.fetch(key) do
serializer_class.cache_store.fetch(key, serializer_class._cache_options) do
attributes(fields, true)
end
key = cache_key(adapter_instance)
cached_attributes.fetch(key) do
fetch(adapter_instance, serializer_class._cache_options, key) do
attributes(fields, true)
end
elsif serializer_class.fragment_cache_enabled?
fetch_attributes_fragment(adapter_instance, cached_attributes)
else
attributes(fields, true)
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
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
end
else
@ -230,7 +240,6 @@ module ActiveModel
# 1. Determine cached fields from serializer class options
# 2. Get non_cached_fields and fetch cache_fields
# 3. Merge the two hashes using adapter_instance#fragment_cache
# rubocop:disable Metrics/AbcSize
def fetch_attributes_fragment(adapter_instance, cached_attributes = {})
serializer_class._cache_options ||= {}
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_hash = attributes(non_cached_fields, true)
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
key = cache_key(adapter_instance)
cached_hash =
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)
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
# Merge both results
adapter_instance.fragment_cache(cached_hash, non_cached_hash)
end
# rubocop:enable Metrics/AbcSize
def cache_key(adapter_instance)
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
class Serializer
# @api private
class HasManyReflection < CollectionReflection
class HasManyReflection < Reflection
def collection?
true
end
end
end
end

View File

@ -1,7 +1,7 @@
module ActiveModel
class Serializer
# @api private
class HasOneReflection < SingularReflection
class HasOneReflection < Reflection
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/association'
module ActiveModel
class Serializer
@ -8,12 +9,26 @@ module ActiveModel
# @example
# class PostSerializer < ActiveModel::Serializer
# has_one :author, serializer: AuthorSerializer
# belongs_to :boss, type: :users, foreign_key: :boss_id
# has_many :comments
# has_many :comments, key: :last_comments do
# object.comments.last(1)
# end
# 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?
# current_user.admin?
# end
@ -23,52 +38,118 @@ module ActiveModel
# 1) as 'comments' and named 'comments'.
# 2) as 'object.comments.last(1)' and named 'last_comments'.
#
# PostSerializer._reflections #=>
# # [
# # HasOneReflection.new(:author, serializer: AuthorSerializer),
# # HasManyReflection.new(:comments)
# # HasManyReflection.new(:comments, { key: :last_comments }, #<Block>)
# # HasManyReflection.new(:secret_meta_data, { if: :is_admin? })
# # ]
# PostSerializer._reflections # =>
# # {
# # author: HasOneReflection.new(:author, serializer: AuthorSerializer),
# # comments: HasManyReflection.new(:comments)
# # last_comments: HasManyReflection.new(:comments, { key: :last_comments }, #<Block>)
# # secret_meta_data: HasManyReflection.new(:secret_meta_data, { if: :is_admin? })
# # }
#
# So you can inspect reflections in your Adapters.
#
class Reflection < Field
attr_reader :foreign_key, :type
def initialize(*)
super
@_links = {}
@_include_data = Serializer.config.include_data_default
@_meta = nil
options[:links] = {}
options[:include_data_setting] = Serializer.config.include_data_default
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
def link(name, value = nil, &block)
@_links[name] = block || value
# @api public
# @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
end
def meta(value = nil, &block)
@_meta = block || value
# @api public
# @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
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)
@_include_data = value
options[:include_data_setting] = value
:nil
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]
# @yield [ActiveModel::Serializer]
# @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)
@object = serializer.object
@scope = serializer.scope
@ -83,6 +164,11 @@ module ActiveModel
end
end
# @api private
def foreign_key_on
:related
end
# Build association. This method is used internally to
# build serializer's association by its reflection.
#
@ -103,61 +189,19 @@ module ActiveModel
# comments_reflection.build_association(post_serializer, foo: 'bar')
#
# @api private
#
def build_association(parent_serializer, parent_serializer_options, include_slice = {})
reflection_options = options.dup
# Pass the parent's namespace onto the child serializer
reflection_options[:namespace] ||= parent_serializer_options[:namespace]
association_value = value(parent_serializer, include_slice)
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)
association_options = {
parent_serializer: parent_serializer,
parent_serializer_options: parent_serializer_options,
include_slice: include_slice
}
Association.new(self, association_options)
end
protected
# used in instance exec
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

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
class Serializer
VERSION = '0.10.5'.freeze
VERSION = '0.10.6'.freeze
end
end

View File

@ -257,7 +257,8 @@ module ActiveModelSerializers
def process_relationships(serializer, include_slice)
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
@ -294,20 +295,8 @@ module ActiveModelSerializers
# {http://jsonapi.org/format/#document-resource-objects Document Resource Objects}
def resource_object_for(serializer, include_slice = {})
resource_object = serializer.fetch(self) do
resource_object = ResourceIdentifier.new(serializer, instance_options).as_json
resource_object = data_for(serializer, include_slice)
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
# definition:
# allOf
@ -321,7 +310,10 @@ module ActiveModelSerializers
# prs:
# https://github.com/rails-api/active_model_serializers/pull/1247
# 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
# alias meta
@ -331,12 +323,33 @@ module ActiveModelSerializers
# {
# :'git-ref' => 'abc123'
# }
meta = meta_for(serializer)
resource_object[:meta] = meta unless meta.blank?
if (meta = meta_for(serializer)).present?
resource_object ||= {}
resource_object[:meta] = meta
end
resource_object
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}
# relationships
# definition:

View File

@ -15,9 +15,7 @@ module ActiveModelSerializers
def as_json
hash = {}
if association.options[:include_data]
hash[:data] = data_for(association)
end
hash[:data] = data_for(association) if association.include_data?
links = links_for(association)
hash[:links] = links if links.any?
@ -35,14 +33,45 @@ module ActiveModelSerializers
private
# TODO(BF): Avoid db hit on belong_to_ releationship by using foreign_key on self
def data_for(association)
serializer = association.serializer
if serializer.respond_to?(:each)
serializer.map { |s| ResourceIdentifier.new(s, serializable_resource_options).as_json }
elsif (virtual_value = association.options[:virtual_value])
if association.collection?
data_for_many(association)
else
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
elsif serializer && serializer.object
ResourceIdentifier.new(serializer, serializable_resource_options).as_json
else
[]
end
end

View File

@ -22,6 +22,14 @@ module ActiveModelSerializers
JsonApi.send(:transform_key_casing!, raw_type, transform_options)
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}
def initialize(serializer, options)
@id = id_for(serializer)
@ -29,6 +37,7 @@ module ActiveModelSerializers
end
def as_json
return nil if id.blank?
{ id: id, type: type }
end

View File

@ -1,12 +1,13 @@
# ActiveModelSerializers::Model is a convenient superclass for making your models
# from Plain-Old Ruby Objects (PORO). It also serves as a reference implementation
# that satisfies ActiveModel::Serializer::Lint::Tests.
require 'active_support/core_ext/hash'
module ActiveModelSerializers
class Model
include ActiveModel::Serializers::JSON
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
# 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.
#
# All attributes to initialize an instance must have setters.
# However, the hash turned by +attributes+ instance method will ALWAYS
# To initialize an instance, all attributes must have setters.
# However, the hash returned by +attributes+ instance method will ALWAYS
# 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
# to mutate the +attributes+ directly.
@ -58,7 +59,7 @@ module ActiveModelSerializers
# 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
# the actual values in the model.
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
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
end
@ -41,7 +41,7 @@ module ActionController
expected = {
data: {
id: @controller.instance_variable_get(:@profile).id.to_s,
id: 'render_using_adapter_override',
type: 'profiles',
attributes: {
name: 'Name 1',

View File

@ -30,18 +30,17 @@ module ActiveModel
def test_has_many_and_has_one
@author_serializer.associations.each do |association|
key = association.key
serializer = association.serializer
options = association.options
serializer = association.lazy_association.serializer
case key
when :posts
assert_equal true, options.fetch(:include_data)
assert_equal true, association.include_data?
assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer)
when :bio
assert_equal true, options.fetch(:include_data)
assert_equal true, association.include_data?
assert_nil serializer
when :roles
assert_equal true, options.fetch(:include_data)
assert_equal true, association.include_data?
assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer)
else
flunk "Unknown association: #{key}"
@ -56,12 +55,11 @@ module ActiveModel
end
post_serializer_class.new(@post).associations.each do |association|
key = association.key
serializer = association.serializer
options = association.options
serializer = association.lazy_association.serializer
assert_equal :tags, key
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
@ -70,7 +68,7 @@ module ActiveModel
.associations
.detect { |assoc| assoc.key == :comments }
comment_serializer = association.serializer.first
comment_serializer = association.lazy_association.serializer.first
class << comment_serializer
def custom_options
instance_options
@ -82,7 +80,7 @@ module ActiveModel
def test_belongs_to
@comment_serializer.associations.each do |association|
key = association.key
serializer = association.serializer
serializer = association.lazy_association.serializer
case key
when :post
@ -93,7 +91,7 @@ module ActiveModel
flunk "Unknown association: #{key}"
end
assert_equal true, association.options.fetch(:include_data)
assert_equal true, association.include_data?
end
end
@ -139,6 +137,34 @@ module ActiveModel
assert expected_association_keys.include? :site
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
has_many :comments
has_many :comments, key: :last_comments do
@ -203,11 +229,11 @@ module ActiveModel
@post_serializer.associations.each do |association|
case association.key
when :comments
assert_instance_of(ResourceNamespace::CommentSerializer, association.serializer.first)
assert_instance_of(ResourceNamespace::CommentSerializer, association.lazy_association.serializer.first)
when :author
assert_instance_of(ResourceNamespace::AuthorSerializer, association.serializer)
assert_instance_of(ResourceNamespace::AuthorSerializer, association.lazy_association.serializer)
when :description
assert_instance_of(ResourceNamespace::DescriptionSerializer, association.serializer)
assert_instance_of(ResourceNamespace::DescriptionSerializer, association.lazy_association.serializer)
else
flunk "Unknown association: #{key}"
end
@ -245,11 +271,11 @@ module ActiveModel
@post_serializer.associations.each do |association|
case association.key
when :comments
assert_instance_of(PostSerializer::CommentSerializer, association.serializer.first)
assert_instance_of(PostSerializer::CommentSerializer, association.lazy_association.serializer.first)
when :author
assert_instance_of(PostSerializer::AuthorSerializer, association.serializer)
assert_instance_of(PostSerializer::AuthorSerializer, association.lazy_association.serializer)
when :description
assert_instance_of(PostSerializer::DescriptionSerializer, association.serializer)
assert_instance_of(PostSerializer::DescriptionSerializer, association.lazy_association.serializer)
else
flunk "Unknown association: #{key}"
end
@ -260,7 +286,7 @@ module ActiveModel
def test_conditional_associations
model = Class.new(::Model) do
attributes :true, :false
associations :association
associations :something
end.new(true: true, false: false)
scenarios = [
@ -284,7 +310,7 @@ module ActiveModel
scenarios.each do |s|
serializer = Class.new(ActiveModel::Serializer) do
belongs_to :association, s[:options]
belongs_to :something, s[:options]
def true
true
@ -296,7 +322,7 @@ module ActiveModel
end
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
@ -341,8 +367,8 @@ module ActiveModel
@author_serializer = AuthorSerializer.new(@author)
@inherited_post_serializer = InheritedPostSerializer.new(@post)
@inherited_author_serializer = InheritedAuthorSerializer.new(@author)
@author_associations = @author_serializer.associations.to_a
@inherited_author_associations = @inherited_author_serializer.associations.to_a
@author_associations = @author_serializer.associations.to_a.sort_by(&:name)
@inherited_author_associations = @inherited_author_serializer.associations.to_a.sort_by(&:name)
@post_associations = @post_serializer.associations.to_a
@inherited_post_associations = @inherited_post_serializer.associations.to_a
end
@ -361,28 +387,35 @@ module ActiveModel
test 'a serializer inheriting from another serializer can redefine has_many and has_one associations' do
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 [true, false, true], @inherited_author_associations.map(&:polymorphic?)
assert_equal [false, false, false], @author_associations.map(&:polymorphic?)
end
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, :comments], @inherited_post_associations.map(&:name)
refute @post_associations.detect { |assoc| assoc.name == :author }.options.key?(:polymorphic)
assert_equal true, @inherited_post_associations.detect { |assoc| assoc.name == :author }.options.fetch(:polymorphic)
refute @post_associations.detect { |assoc| assoc.name == :author }.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 }
refute original_comment_assoc.options.key?(:key)
assert_equal :reviews, new_comments_assoc.options.fetch(:key)
refute original_comment_assoc.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
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
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)
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