mirror of
https://github.com/ditkrg/active_model_serializers.git
synced 2026-01-23 06:16:50 +00:00
Merge branch 'master' into 0-10-stable
This commit is contained in:
commit
b48aeeef1e
@ -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
|
||||
|
||||
18
CHANGELOG.md
18
CHANGELOG.md
@ -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)
|
||||
|
||||
@ -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)
|
||||
- [](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)
|
||||
- [](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)
|
||||
- [](http://www.rubydoc.info/github/rails-api/active_model_serializers/0-9-stable)
|
||||
|
||||
31
Rakefile
31
Rakefile
@ -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|
|
||||
|
||||
@ -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
38
bin/rubocop
Executable 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
|
||||
@ -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):
|
||||
|
||||
@ -37,7 +37,7 @@ and
|
||||
class CommentSerializer < ActiveModel::Serializer
|
||||
attributes :name, :body
|
||||
|
||||
belongs_to :post_id
|
||||
belongs_to :post
|
||||
end
|
||||
```
|
||||
|
||||
|
||||
@ -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`
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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.
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
# @api private
|
||||
class CollectionReflection < Reflection
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -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
|
||||
@ -1,7 +1,10 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
# @api private
|
||||
class HasManyReflection < CollectionReflection
|
||||
class HasManyReflection < Reflection
|
||||
def collection?
|
||||
true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
# @api private
|
||||
class HasOneReflection < SingularReflection
|
||||
class HasOneReflection < Reflection
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
95
lib/active_model/serializer/lazy_association.rb
Normal file
95
lib/active_model/serializer/lazy_association.rb
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
# @api private
|
||||
class SingularReflection < Reflection
|
||||
end
|
||||
end
|
||||
end
|
||||
@ -1,5 +1,5 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
VERSION = '0.10.5'.freeze
|
||||
VERSION = '0.10.6'.freeze
|
||||
end
|
||||
end
|
||||
|
||||
@ -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:
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
53
lib/tasks/rubocop.rake
Normal 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
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
|
||||
427
test/serializers/reflection_test.rb
Normal file
427
test/serializers/reflection_test.rb
Normal 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
|
||||
Loading…
Reference in New Issue
Block a user