From 24c0212c83260d1074c27ab296028824f3f49099 Mon Sep 17 00:00:00 2001 From: Matt Gardner Date: Wed, 8 Mar 2017 18:04:35 -0500 Subject: [PATCH 01/47] Providing caveat in documentation (#2070) * Providing caveat in documentation I think it'd be helpful to mention that `jsonapi_parse!` will throw an InvalidDocument error. * Update ember-and-json-api.md --- docs/integrations/ember-and-json-api.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/integrations/ember-and-json-api.md b/docs/integrations/ember-and-json-api.md index 57454b72..eb7f1ade 100644 --- a/docs/integrations/ember-and-json-api.md +++ b/docs/integrations/ember-and-json-api.md @@ -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 From a36b25d2db6734ed61158559b564ccec4e4f7f87 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 12 Mar 2017 15:50:05 -0500 Subject: [PATCH 02/47] Add rubocop binstub that rspects file patterns Best of both worlds! (Because you can't override the default rubocop includes) The binstub basically, lets me safely `rubocop test/foo_test.rb` instead of `bundle exec rubocop test/foo_test.rb` ```bash # ~/.profile # https://twitter.com/tpope/status/165631968996900865 # tl;dr `mkdir .git/safe` to add `bin` to path, e.g. `bin/rails` PATH=".git/safe/../../bin:$PATH" ``` --- .rubocop.yml | 5 +++- Rakefile | 31 +----------------------- bin/rubocop | 38 ++++++++++++++++++++++++++++++ lib/tasks/rubocop.rake | 53 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 96 insertions(+), 31 deletions(-) create mode 100755 bin/rubocop create mode 100644 lib/tasks/rubocop.rake diff --git a/.rubocop.yml b/.rubocop.yml index 8d7551a5..82f07656 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -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 diff --git a/Rakefile b/Rakefile index 912c7fcb..6ba0c2bc 100644 --- a/Rakefile +++ b/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| diff --git a/bin/rubocop b/bin/rubocop new file mode 100755 index 00000000..269f8954 --- /dev/null +++ b/bin/rubocop @@ -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 diff --git a/lib/tasks/rubocop.rake b/lib/tasks/rubocop.rake new file mode 100644 index 00000000..5c9a1242 --- /dev/null +++ b/lib/tasks/rubocop.rake @@ -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 From 9c26ffe2d6ba70981f2ffba4c6610ed0c75ac07d Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Mon, 27 Feb 2017 22:27:36 -0600 Subject: [PATCH 03/47] Better variables; allow looking serializer from class --- lib/active_model/serializer.rb | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 0d94bfb5..54a3724d 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -34,17 +34,18 @@ module ActiveModel # @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 From 47e82e09b10fd50b718fd71c54bd2d6a13dc376b Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 12 Mar 2017 20:18:48 -0500 Subject: [PATCH 04/47] Make behavior explicit --- lib/active_model/serializer.rb | 2 ++ 1 file changed, 2 insertions(+) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 54a3724d..9d779008 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -92,6 +92,8 @@ module ActiveModel serializer_class elsif klass.superclass get_serializer_for(klass.superclass) + else + nil # No serializer found end end end From c377b7e31de78bbc1a1da1f284a4868da750b7f8 Mon Sep 17 00:00:00 2001 From: lvela Date: Mon, 13 Mar 2017 14:21:56 -0500 Subject: [PATCH 05/47] Correct info on using `JSON` adapter I think this needs to be changed (based on info above). --- docs/howto/add_pagination_links.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/howto/add_pagination_links.md b/docs/howto/add_pagination_links.md index 69d290c2..e2792383 100644 --- a/docs/howto/add_pagination_links.md +++ b/docs/howto/add_pagination_links.md @@ -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. From 36b4eac79b4d1ad6bd24dcf5996ba7f9956c53cb Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 12 Mar 2017 21:38:07 -0500 Subject: [PATCH 06/47] Make serializer interface more obvious --- lib/active_model/serializer.rb | 245 +++++++++++++++++- .../serializer/concerns/associations.rb | 102 -------- .../serializer/concerns/attributes.rb | 82 ------ .../serializer/concerns/caching.rb | 2 +- .../serializer/concerns/configuration.rb | 59 ----- lib/active_model/serializer/concerns/links.rb | 35 --- lib/active_model/serializer/concerns/meta.rb | 29 --- lib/active_model/serializer/concerns/type.rb | 25 -- 8 files changed, 234 insertions(+), 345 deletions(-) delete mode 100644 lib/active_model/serializer/concerns/associations.rb delete mode 100644 lib/active_model/serializer/concerns/attributes.rb delete mode 100644 lib/active_model/serializer/concerns/configuration.rb delete mode 100644 lib/active_model/serializer/concerns/links.rb delete mode 100644 lib/active_model/serializer/concerns/meta.rb delete mode 100644 lib/active_model/serializer/concerns/type.rb diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 0d94bfb5..597b34c8 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -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' @@ -23,13 +17,16 @@ module ActiveModel 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] @@ -111,6 +108,200 @@ module ActiveModel @serialization_adapter_instance ||= ActiveModelSerializers::Adapter::Attributes end + # Configuration options may also be set in + # Serializers and Adapters + config.collection_serializer = ActiveModel::Serializer::CollectionSerializer + config.serializer_lookup_enabled = true + + # @deprecated Use {#config.collection_serializer=} instead of this. Is + # compatibilty layer for ArraySerializer. + def config.array_serializer=(collection_serializer) + self.collection_serializer = collection_serializer + end + + # @deprecated Use {#config.collection_serializer} instead of this. Is + # compatibilty layer for ArraySerializer. + def config.array_serializer + collection_serializer + end + + config.default_includes = '*' + 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' + + 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 + + # keys of attributes + # @see Serializer::attribute + def self._attributes + _attributes_data.keys + end + + # @example + # class AdminAuthorSerializer < ActiveModel::Serializer + # attributes :id, :name, :recent_edits + def self.attributes(*attrs) + attrs = attrs.first if attrs.first.class == Array + + attrs.each do |attr| + attribute(attr) + end + end + + # @example + # class AdminAuthorSerializer < ActiveModel::Serializer + # attributes :id, :recent_edits + # attribute :name, key: :title + # + # 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 + + # @api private + # maps attribute value to explicit key name + # @see Serializer::attribute + # @see ActiveModel::Serializer::Caching#fragmented_attributes + def self._attributes_keys + _attributes_data + .each_with_object({}) do |(key, attr), hash| + next if key == attr.name + hash[attr.name] = { key: key } + end + end + + # @param [Symbol] name of the association + # @param [Hash 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 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 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 + attr_accessor :object, :root, :scope # `scope_name` is set as :current_user by default in the controller. @@ -131,6 +322,36 @@ 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] + # + 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 + # @return [Hash] containing the attributes and first level # associations, similar to how ActiveModel::Serializers::JSON is used # in ActiveRecord::Base. diff --git a/lib/active_model/serializer/concerns/associations.rb b/lib/active_model/serializer/concerns/associations.rb deleted file mode 100644 index ce0ea21f..00000000 --- a/lib/active_model/serializer/concerns/associations.rb +++ /dev/null @@ -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 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 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 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] - # - 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 diff --git a/lib/active_model/serializer/concerns/attributes.rb b/lib/active_model/serializer/concerns/attributes.rb deleted file mode 100644 index 6ee2732f..00000000 --- a/lib/active_model/serializer/concerns/attributes.rb +++ /dev/null @@ -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 diff --git a/lib/active_model/serializer/concerns/caching.rb b/lib/active_model/serializer/concerns/caching.rb index 4809f4cb..c8340787 100644 --- a/lib/active_model/serializer/concerns/caching.rb +++ b/lib/active_model/serializer/concerns/caching.rb @@ -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 diff --git a/lib/active_model/serializer/concerns/configuration.rb b/lib/active_model/serializer/concerns/configuration.rb deleted file mode 100644 index d6d3c610..00000000 --- a/lib/active_model/serializer/concerns/configuration.rb +++ /dev/null @@ -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 diff --git a/lib/active_model/serializer/concerns/links.rb b/lib/active_model/serializer/concerns/links.rb deleted file mode 100644 index 1322adb0..00000000 --- a/lib/active_model/serializer/concerns/links.rb +++ /dev/null @@ -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 diff --git a/lib/active_model/serializer/concerns/meta.rb b/lib/active_model/serializer/concerns/meta.rb deleted file mode 100644 index 5160585e..00000000 --- a/lib/active_model/serializer/concerns/meta.rb +++ /dev/null @@ -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 diff --git a/lib/active_model/serializer/concerns/type.rb b/lib/active_model/serializer/concerns/type.rb deleted file mode 100644 index c37c9af8..00000000 --- a/lib/active_model/serializer/concerns/type.rb +++ /dev/null @@ -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 From 2e71bc47f42a63389ea54fd5d2925d0b7dabc45e Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Thu, 16 Mar 2017 10:14:18 -0500 Subject: [PATCH 07/47] Improve comments; move caching concern to caching.rb --- lib/active_model/serializer.rb | 23 +++++++------------ .../serializer/concerns/caching.rb | 12 ++++++++++ 2 files changed, 20 insertions(+), 15 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 597b34c8..d77eb7e3 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -108,8 +108,8 @@ module ActiveModel @serialization_adapter_instance ||= ActiveModelSerializers::Adapter::Attributes end - # Configuration options may also be set in - # Serializers and Adapters + # Preferred interface is ActiveModelSerializers.config + # BEGIN DEFAULT CONFIGURATION config.collection_serializer = ActiveModel::Serializer::CollectionSerializer config.serializer_lookup_enabled = true @@ -159,6 +159,7 @@ module ActiveModel 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 @@ -180,12 +181,14 @@ module ActiveModel base._links = _links.dup end - # keys of attributes + # @return [Array] 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 @@ -214,18 +217,6 @@ module ActiveModel _attributes_data[key] = Attribute.new(attr, options, block) end - # @api private - # maps attribute value to explicit key name - # @see Serializer::attribute - # @see ActiveModel::Serializer::Caching#fragmented_attributes - def self._attributes_keys - _attributes_data - .each_with_object({}) do |(key, attr), hash| - next if key == attr.name - hash[attr.name] = { key: key } - end - end - # @param [Symbol] name of the association # @param [Hash any>] options for the reflection # @return [void] @@ -302,6 +293,8 @@ module ActiveModel 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. diff --git a/lib/active_model/serializer/concerns/caching.rb b/lib/active_model/serializer/concerns/caching.rb index c8340787..3238adc8 100644 --- a/lib/active_model/serializer/concerns/caching.rb +++ b/lib/active_model/serializer/concerns/caching.rb @@ -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) } From cec6478f322bf90383b2b6e084a120639cc1af4f Mon Sep 17 00:00:00 2001 From: Ryunosuke Sato Date: Fri, 24 Mar 2017 10:40:27 +0900 Subject: [PATCH 08/47] Fix example code in `doc/general/getting_started.md` The `belongs_to` method should take relation name, not a foreign_key property. --- docs/general/getting_started.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/general/getting_started.md b/docs/general/getting_started.md index ac6d5f79..b39cd283 100644 --- a/docs/general/getting_started.md +++ b/docs/general/getting_started.md @@ -37,7 +37,7 @@ and class CommentSerializer < ActiveModel::Serializer attributes :name, :body - belongs_to :post_id + belongs_to :post end ``` From f327b6be0c324d6b8122226a10fec6f6ea567107 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Mon, 27 Mar 2017 21:43:16 -0500 Subject: [PATCH 09/47] Improve reflection internal interface --- lib/active_model/serializer.rb | 4 +- lib/active_model/serializer/reflection.rb | 102 +++++++++++++++++----- 2 files changed, 80 insertions(+), 26 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 978f352b..ea24ce52 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -117,13 +117,13 @@ module ActiveModel config.serializer_lookup_enabled = true # @deprecated Use {#config.collection_serializer=} instead of this. Is - # compatibilty layer for ArraySerializer. + # 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 - # compatibilty layer for ArraySerializer. + # compatibility layer for ArraySerializer. def config.array_serializer collection_serializer end diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index 96645bd7..96cca0be 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -14,6 +14,19 @@ module ActiveModel # 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 @@ -32,43 +45,82 @@ module ActiveModel # # ] # # So you can inspect reflections in your Adapters. - # class Reflection < Field 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 end + # @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, &block) - @_links[name] = block || value + options[:links][name] = block || value :nil end + # @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, &block) - @_meta = block || value + options[:meta] = block || 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 # @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 @@ -103,7 +155,6 @@ 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 @@ -113,8 +164,8 @@ module ActiveModel 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 + reflection_options[:links] = options[:links] + reflection_options[:meta] = options[:meta] if serializer_class serializer = catch(:no_serializer) do @@ -138,15 +189,18 @@ module ActiveModel 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 + 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 From 1a5e66b933f20c2abf4ba399a9803d11ea83dc98 Mon Sep 17 00:00:00 2001 From: Timur Date: Tue, 28 Mar 2017 13:14:50 +0600 Subject: [PATCH 10/47] [0.10] add docs for include (#2081) * Add docs for `include` option in the adapter --- CHANGELOG.md | 2 ++ docs/general/adapters.md | 34 +++++++++++++++++++++++++--------- 2 files changed, 27 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f6ea953..a4685842 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ Fixes: Misc: +- [#2081](https://github.com/rails-api/active_model_serializers/pull/2081) Documentation for `include` option in adapters. (@charlie-wasp) + ### [v0.10.5 (2017-03-07)](https://github.com/rails-api/active_model_serializers/compare/v0.10.4...v0.10.5) Breaking changes: diff --git a/docs/general/adapters.md b/docs/general/adapters.md index 6ae1d27a..84fc4e62 100644 --- a/docs/general/adapters.md +++ b/docs/general/adapters.md @@ -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): From b2f5f32036c1d6dbe0f643e2978a303d1473b8f7 Mon Sep 17 00:00:00 2001 From: Mike Kelly Date: Tue, 28 Mar 2017 09:44:53 -0400 Subject: [PATCH 11/47] Reword ActiveModelSerializer::Model docs for clarity Fixed some typos, and reworked a sentence to be clearer. --- lib/active_model_serializers/model.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/lib/active_model_serializers/model.rb b/lib/active_model_serializers/model.rb index e3c86e98..ea10ac88 100644 --- a/lib/active_model_serializers/model.rb +++ b/lib/active_model_serializers/model.rb @@ -6,7 +6,7 @@ module ActiveModelSerializers 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 +19,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 +58,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 From 729882caaae6ed73d876e6167d795ef8c556fe55 Mon Sep 17 00:00:00 2001 From: Cassidy K Date: Sun, 16 Apr 2017 10:03:43 -0400 Subject: [PATCH 12/47] Modifying gemspec to use grape v0.19.1 --- active_model_serializers.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/active_model_serializers.gemspec b/active_model_serializers.gemspec index 108f166a..805c99c8 100644 --- a/active_model_serializers.gemspec +++ b/active_model_serializers.gemspec @@ -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 From c6291b3c9171019fc0f913979154cd343e3c0401 Mon Sep 17 00:00:00 2001 From: Tony Ta Date: Tue, 18 Apr 2017 11:44:24 -0700 Subject: [PATCH 13/47] points to correct latest version --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 59a8c854..d069dcf5 100644 --- a/README.md +++ b/README.md @@ -90,8 +90,8 @@ reading documentation for our `master`, which may include features that have not been released yet. Please see below for the documentation relevant to you. - [0.10 (master) Documentation](https://github.com/rails-api/active_model_serializers/tree/master) -- [0.10.4 (latest release) Documentation](https://github.com/rails-api/active_model_serializers/tree/v0.10.4) - - [![API Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/gems/active_model_serializers/0.10.4) +- [0.10.5 (latest release) Documentation](https://github.com/rails-api/active_model_serializers/tree/v0.10.5) + - [![API Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/gems/active_model_serializers/0.10.5) - [Guides](docs) - [0.9 (0-9-stable) Documentation](https://github.com/rails-api/active_model_serializers/tree/0-9-stable) - [![API Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/github/rails-api/active_model_serializers/0-9-stable) From aa619b5e0e571ae94c586d57fd182c0f6cf4e628 Mon Sep 17 00:00:00 2001 From: Cassidy K Date: Tue, 11 Apr 2017 16:01:21 -0400 Subject: [PATCH 14/47] Update Serializers and Rendering Docs - Updating general/serializers.md - Updating docs/general/rendering.md - adding to changelog - Updating rendering.md to indicate that `each_serializer` must be used on a collection - updating my handle in previous changelog entry --- CHANGELOG.md | 3 ++- docs/general/rendering.md | 22 ++++++++++++++++++---- docs/general/serializers.md | 19 +++++++++++++++++-- 3 files changed, 37 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4685842..fc263078 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Fixes: 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) ### [v0.10.5 (2017-03-07)](https://github.com/rails-api/active_model_serializers/compare/v0.10.4...v0.10.5) @@ -79,7 +80,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) diff --git a/docs/general/rendering.md b/docs/general/rendering.md index 21120a5a..af2d886f 100644 --- a/docs/general/rendering.md +++ b/docs/general/rendering.md @@ -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` diff --git a/docs/general/serializers.md b/docs/general/serializers.md index bb6e21be..1d968183 100644 --- a/docs/general/serializers.md +++ b/docs/general/serializers.md @@ -382,11 +382,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 From 1ef7c7d35ba8a8822511329d75370f15cf41bada Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Thu, 30 Mar 2017 22:47:53 -0500 Subject: [PATCH 15/47] Add reflection tests --- test/serializers/reflection_test.rb | 218 ++++++++++++++++++++++++++++ 1 file changed, 218 insertions(+) create mode 100644 test/serializers/reflection_test.rb diff --git a/test/serializers/reflection_test.rb b/test/serializers/reflection_test.rb new file mode 100644 index 00000000..6fa88687 --- /dev/null +++ b/test/serializers/reflection_test.rb @@ -0,0 +1,218 @@ +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 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.instance_variable_get(:@_links) + + # Build Association + association = reflection.build_association(serializer_instance, @instance_options) + assert_equal @expected_links, association.links + assert_equal @expected_links, reflection.instance_variable_get(:@_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.instance_variable_get(:@_links) + + # Build Association + association = reflection.build_association(serializer_instance, @instance_options) + # Assert before instance_eval link + link = association.links.fetch(:self) + assert_respond_to link, :call + + # Assert after instance_eval link + assert_equal @expected_links.fetch(:self), reflection.instance_eval(&link) + assert_respond_to reflection.instance_variable_get(:@_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.instance_variable_get(:@_meta) + + # Build Association + association = reflection.build_association(serializer_instance, @instance_options) + assert_equal @expected_meta, association.meta + assert_equal @expected_meta, reflection.instance_variable_get(:@_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.instance_variable_get(:@_meta) + + # Build Association + association = reflection.build_association(serializer_instance, @instance_options) + # Assert before instance_eval meta + assert_respond_to association.meta, :call + assert_respond_to reflection.instance_variable_get(:@_meta), :call + + # Assert after instance_eval meta + assert_equal @expected_meta, reflection.instance_eval(&association.meta) + assert_respond_to reflection.instance_variable_get(:@_meta), :call + assert_respond_to association.meta, :call + end + + 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.instance_variable_get(:@_meta) + assert_equal @empty_links, reflection.instance_variable_get(:@_links) + + # Build Association + association = reflection.build_association(serializer_instance, @instance_options) + # Assert before instance_eval link meta + assert_nil association.meta + assert_nil reflection.instance_variable_get(:@_meta) + + link = association.links.fetch(:self) + assert_respond_to link, :call + assert_respond_to reflection.instance_variable_get(:@_links).fetch(:self), :call + assert_nil reflection.instance_variable_get(:@_meta) + + # Assert after instance_eval link + assert_equal 'no_uri_validation', reflection.instance_eval(&link) + assert_equal @expected_meta, reflection.instance_variable_get(:@_meta) + assert_nil association.meta + end + + # 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.instance_variable_get(:@_meta) + + # Build Association + association = reflection.build_association(serializer_instance, @instance_options) + assert_nil association.meta + assert_nil reflection.instance_variable_get(:@_meta) + + # Assert before instance_eval link + link = association.links.fetch(:self) + assert_nil reflection.instance_variable_get(:@_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.instance_variable_get(:@_meta), :call + assert_nil association.meta + + # Assert after instance_eval link meta + assert_equal @expected_meta, reflection.instance_eval(&reflection.instance_variable_get(:@_meta)) + assert_nil association.meta + 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.instance_variable_get(:@_links) + + # Build Association + association = reflection.build_association(serializer_instance, @instance_options) + # Assert before instance_eval link + 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 + end + end +end From 629aa8c7b177ddc18d3684f509d69f6551dfc72b Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sat, 22 Apr 2017 22:06:08 -0500 Subject: [PATCH 16/47] Correct tests since reflections changes --- test/serializers/reflection_test.rb | 42 ++++++++++++++--------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/test/serializers/reflection_test.rb b/test/serializers/reflection_test.rb index 6fa88687..5619d151 100644 --- a/test/serializers/reflection_test.rb +++ b/test/serializers/reflection_test.rb @@ -35,12 +35,12 @@ module ActiveModel # Get Reflection reflection = serializer_class._reflections.fetch(:blog) - assert_equal @empty_links, reflection.instance_variable_get(:@_links) + assert_equal @empty_links, reflection.options.fetch(:links) # Build Association association = reflection.build_association(serializer_instance, @instance_options) assert_equal @expected_links, association.links - assert_equal @expected_links, reflection.instance_variable_get(:@_links) + assert_equal @expected_links, reflection.options.fetch(:links) end def test_reflection_block_with_link_block_mutates_the_reflection_links @@ -55,7 +55,7 @@ module ActiveModel # Get Reflection reflection = serializer_class._reflections.fetch(:blog) - assert_equal @empty_links, reflection.instance_variable_get(:@_links) + assert_equal @empty_links, reflection.options.fetch(:links) # Build Association association = reflection.build_association(serializer_instance, @instance_options) @@ -65,7 +65,7 @@ module ActiveModel # Assert after instance_eval link assert_equal @expected_links.fetch(:self), reflection.instance_eval(&link) - assert_respond_to reflection.instance_variable_get(:@_links).fetch(:self), :call + assert_respond_to reflection.options.fetch(:links).fetch(:self), :call end def test_reflection_block_with_meta_mutates_the_reflection_meta @@ -78,12 +78,12 @@ module ActiveModel # Get Reflection reflection = serializer_class._reflections.fetch(:blog) - assert_nil reflection.instance_variable_get(:@_meta) + assert_nil reflection.options.fetch(:meta) # Build Association association = reflection.build_association(serializer_instance, @instance_options) assert_equal @expected_meta, association.meta - assert_equal @expected_meta, reflection.instance_variable_get(:@_meta) + assert_equal @expected_meta, reflection.options.fetch(:meta) end def test_reflection_block_with_meta_block_mutates_the_reflection_meta @@ -98,17 +98,17 @@ module ActiveModel # Get Reflection reflection = serializer_class._reflections.fetch(:blog) - assert_nil reflection.instance_variable_get(:@_meta) + assert_nil reflection.options.fetch(:meta) # Build Association association = reflection.build_association(serializer_instance, @instance_options) # Assert before instance_eval meta assert_respond_to association.meta, :call - assert_respond_to reflection.instance_variable_get(:@_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.instance_variable_get(:@_meta), :call + assert_respond_to reflection.options.fetch(:meta), :call assert_respond_to association.meta, :call end @@ -125,23 +125,23 @@ module ActiveModel # Get Reflection reflection = serializer_class._reflections.fetch(:blog) - assert_nil reflection.instance_variable_get(:@_meta) - assert_equal @empty_links, reflection.instance_variable_get(:@_links) + 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.instance_variable_get(:@_meta) + assert_nil reflection.options.fetch(:meta) link = association.links.fetch(:self) assert_respond_to link, :call - assert_respond_to reflection.instance_variable_get(:@_links).fetch(:self), :call - assert_nil reflection.instance_variable_get(:@_meta) + 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.instance_variable_get(:@_meta) + assert_equal @expected_meta, reflection.options.fetch(:meta) assert_nil association.meta end @@ -161,16 +161,16 @@ module ActiveModel # Get Reflection reflection = serializer_class._reflections.fetch(:blog) - assert_nil reflection.instance_variable_get(:@_meta) + assert_nil reflection.options.fetch(:meta) # Build Association association = reflection.build_association(serializer_instance, @instance_options) assert_nil association.meta - assert_nil reflection.instance_variable_get(:@_meta) + assert_nil reflection.options.fetch(:meta) # Assert before instance_eval link link = association.links.fetch(:self) - assert_nil reflection.instance_variable_get(:@_meta) + assert_nil reflection.options.fetch(:meta) assert_respond_to link, :call assert_respond_to association.links.fetch(:self), :call @@ -178,11 +178,11 @@ module ActiveModel 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.instance_variable_get(:@_meta), :call + assert_respond_to reflection.options.fetch(:meta), :call assert_nil association.meta # Assert after instance_eval link meta - assert_equal @expected_meta, reflection.instance_eval(&reflection.instance_variable_get(:@_meta)) + assert_equal @expected_meta, reflection.instance_eval(&reflection.options.fetch(:meta)) assert_nil association.meta end # rubocop:enable Metrics/AbcSize @@ -199,7 +199,7 @@ module ActiveModel # Get Reflection reflection = serializer_class._reflections.fetch(:blog) - assert_equal @empty_links, reflection.instance_variable_get(:@_links) + assert_equal @empty_links, reflection.options.fetch(:links) # Build Association association = reflection.build_association(serializer_instance, @instance_options) From e07613b63fb7f4d7cd3f05af82624f2c4c53a5f3 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Fri, 31 Mar 2017 00:11:52 -0500 Subject: [PATCH 17/47] Assert mutating reflection is not thread-safe --- test/serializers/reflection_test.rb | 31 +++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/test/serializers/reflection_test.rb b/test/serializers/reflection_test.rb index 5619d151..1f0efd94 100644 --- a/test/serializers/reflection_test.rb +++ b/test/serializers/reflection_test.rb @@ -213,6 +213,37 @@ module ActiveModel end assert_match(/undefined method `href'/, exception.message) end + + 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.instance_variable_get(:@_meta) + association = reflection.build_association(serializer_instance, @instance_options) + assert_equal model1_meta, association.meta + assert_equal model1_meta, reflection.instance_variable_get(:@_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.instance_variable_get(:@_meta) + + association = reflection.build_association(serializer_instance, @instance_options) + assert_equal model2_meta, association.meta + assert_equal model2_meta, reflection.instance_variable_get(:@_meta) + end end end end From 844045500295903efa3bf54b2e2b16d338d289fe Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sat, 22 Apr 2017 22:06:59 -0500 Subject: [PATCH 18/47] Correct tests since reflections changes --- test/serializers/reflection_test.rb | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/serializers/reflection_test.rb b/test/serializers/reflection_test.rb index 1f0efd94..ffe09444 100644 --- a/test/serializers/reflection_test.rb +++ b/test/serializers/reflection_test.rb @@ -224,10 +224,10 @@ module ActiveModel # 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.instance_variable_get(:@_meta) + assert_nil reflection.options.fetch(:meta) association = reflection.build_association(serializer_instance, @instance_options) assert_equal model1_meta, association.meta - assert_equal model1_meta, reflection.instance_variable_get(:@_meta) + assert_equal model1_meta, reflection.options.fetch(:meta) model2_meta = @expected_meta.merge(id: 2) # Evaluate reflection meta for model with id 2 @@ -238,11 +238,11 @@ module ActiveModel # WARN: Thread-safety issue # Before the reflection is evaluated, it has the value from the previous evaluation - assert_equal model1_meta, reflection.instance_variable_get(:@_meta) + assert_equal model1_meta, reflection.options.fetch(:meta) association = reflection.build_association(serializer_instance, @instance_options) assert_equal model2_meta, association.meta - assert_equal model2_meta, reflection.instance_variable_get(:@_meta) + assert_equal model2_meta, reflection.options.fetch(:meta) end end end From 810229656d40d2e6aa9630254dfb8323a4d79f8b Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Fri, 31 Mar 2017 11:41:58 -0500 Subject: [PATCH 19/47] Test Reflection value/include_data --- test/serializers/reflection_test.rb | 119 ++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/test/serializers/reflection_test.rb b/test/serializers/reflection_test.rb index ffe09444..a0697eea 100644 --- a/test/serializers/reflection_test.rb +++ b/test/serializers/reflection_test.rb @@ -25,6 +25,125 @@ module ActiveModel @instance_options = {} end + 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.instance_variable_get(:@_include_data) + assert_equal true, reflection.instance_variable_get(:@_include_data) + + include_slice = :does_not_matter + assert_equal @model.blog, reflection.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.instance_variable_get(:@_include_data) + assert_equal true, reflection.instance_variable_get(:@_include_data) + + include_slice = :does_not_matter + assert_equal @model.blog, reflection.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.instance_variable_get(:@_include_data) + assert_equal true, reflection.instance_variable_get(:@_include_data) + + include_slice = :does_not_matter + assert_equal @model.blog, reflection.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.instance_variable_get(:@_include_data) + include_slice = :does_not_matter + assert_nil reflection.value(serializer_instance, include_slice) + assert_equal false, reflection.instance_variable_get(:@_include_data) + 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.instance_variable_get(:@_include_data) + include_slice = {} + assert_nil reflection.value(serializer_instance, include_slice) + assert_equal :if_sideloaded, reflection.instance_variable_get(:@_include_data) + 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.instance_variable_get(:@_include_data) + include_slice = { blog: :does_not_matter } + assert_equal @model.blog, reflection.value(serializer_instance, include_slice) + assert_equal :if_sideloaded, reflection.instance_variable_get(:@_include_data) + end + def test_reflection_block_with_link_mutates_the_reflection_links serializer_class = Class.new(ActiveModel::Serializer) do has_one :blog do From b4cef58e98fe5990ac2c08a01d1ad5b32d49dd63 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sat, 22 Apr 2017 22:08:20 -0500 Subject: [PATCH 20/47] Correct tests since reflections changes --- test/serializers/reflection_test.rb | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/test/serializers/reflection_test.rb b/test/serializers/reflection_test.rb index a0697eea..8f157bb9 100644 --- a/test/serializers/reflection_test.rb +++ b/test/serializers/reflection_test.rb @@ -36,8 +36,8 @@ module ActiveModel # Assert assert_nil reflection.block - assert_equal Serializer.config.include_data_default, reflection.instance_variable_get(:@_include_data) - assert_equal true, reflection.instance_variable_get(:@_include_data) + 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.value(serializer_instance, include_slice) @@ -56,8 +56,8 @@ module ActiveModel # Assert assert_respond_to reflection.block, :call - assert_equal Serializer.config.include_data_default, reflection.instance_variable_get(:@_include_data) - assert_equal true, reflection.instance_variable_get(:@_include_data) + 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.value(serializer_instance, include_slice) @@ -77,8 +77,8 @@ module ActiveModel # Assert assert_respond_to reflection.block, :call - assert_equal Serializer.config.include_data_default, reflection.instance_variable_get(:@_include_data) - assert_equal true, reflection.instance_variable_get(:@_include_data) + 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.value(serializer_instance, include_slice) @@ -98,10 +98,10 @@ module ActiveModel # Assert assert_respond_to reflection.block, :call - assert_equal true, reflection.instance_variable_get(:@_include_data) + assert_equal true, reflection.options.fetch(:include_data_setting) include_slice = :does_not_matter assert_nil reflection.value(serializer_instance, include_slice) - assert_equal false, reflection.instance_variable_get(:@_include_data) + 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 @@ -118,10 +118,10 @@ module ActiveModel # Assert assert_respond_to reflection.block, :call - assert_equal true, reflection.instance_variable_get(:@_include_data) + assert_equal true, reflection.options.fetch(:include_data_setting) include_slice = {} assert_nil reflection.value(serializer_instance, include_slice) - assert_equal :if_sideloaded, reflection.instance_variable_get(:@_include_data) + 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 @@ -138,10 +138,10 @@ module ActiveModel # Assert assert_respond_to reflection.block, :call - assert_equal true, reflection.instance_variable_get(:@_include_data) + assert_equal true, reflection.options.fetch(:include_data_setting) include_slice = { blog: :does_not_matter } assert_equal @model.blog, reflection.value(serializer_instance, include_slice) - assert_equal :if_sideloaded, reflection.instance_variable_get(:@_include_data) + assert_equal :if_sideloaded, reflection.options.fetch(:include_data_setting) end def test_reflection_block_with_link_mutates_the_reflection_links From c13354c4e82dddbd6903245efd1432e1bcac2f70 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Fri, 31 Mar 2017 11:51:58 -0500 Subject: [PATCH 21/47] Add test todos before I forget --- test/serializers/reflection_test.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/test/serializers/reflection_test.rb b/test/serializers/reflection_test.rb index 8f157bb9..6a0bd23d 100644 --- a/test/serializers/reflection_test.rb +++ b/test/serializers/reflection_test.rb @@ -25,6 +25,23 @@ module ActiveModel @instance_options = {} 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 From 758e44e6e2ce6a50decbb2b0038209a79fb80cc0 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sat, 22 Apr 2017 22:10:43 -0500 Subject: [PATCH 22/47] Style fixes --- test/serializers/reflection_test.rb | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/test/serializers/reflection_test.rb b/test/serializers/reflection_test.rb index 6a0bd23d..e5932aba 100644 --- a/test/serializers/reflection_test.rb +++ b/test/serializers/reflection_test.rb @@ -248,6 +248,7 @@ module ActiveModel 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 @@ -280,6 +281,7 @@ module ActiveModel assert_equal @expected_meta, reflection.options.fetch(:meta) assert_nil 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 @@ -350,6 +352,7 @@ module ActiveModel 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 @@ -380,6 +383,7 @@ module ActiveModel assert_equal model2_meta, association.meta assert_equal model2_meta, reflection.options.fetch(:meta) end + # rubocop:enable Metrics/AbcSize end end end From c2dccbac5f85332dba437342502fdae8fd44c7d7 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Fri, 31 Mar 2017 13:09:51 -0500 Subject: [PATCH 23/47] Move attributes cache method out of concern --- lib/active_model/serializer.rb | 14 +++++++++-- .../serializer/concerns/caching.rb | 23 +++++++------------ 2 files changed, 20 insertions(+), 17 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index ea24ce52..a9ecb1e3 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -379,8 +379,7 @@ module ActiveModel 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) + resource = attributes_hash(adapter_options, options, adapter_instance) relationships = resource_relationships(adapter_options, options, adapter_instance) resource.merge(relationships) end @@ -412,6 +411,17 @@ module ActiveModel end end + # @api private + 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 + end + # @api private def resource_relationships(adapter_options, options, adapter_instance) relationships = {} diff --git a/lib/active_model/serializer/concerns/caching.rb b/lib/active_model/serializer/concerns/caching.rb index 3238adc8..69900001 100644 --- a/lib/active_model/serializer/concerns/caching.rb +++ b/lib/active_model/serializer/concerns/caching.rb @@ -170,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? @@ -215,23 +216,17 @@ 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 = cache_key(adapter_instance)) if serializer_class.cache_store - serializer_class.cache_store.fetch(cache_key(adapter_instance), cache_options) do + serializer_class.cache_store.fetch(key, cache_options) do yield end else @@ -242,7 +237,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 @@ -257,7 +251,7 @@ module ActiveModel 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) @@ -266,7 +260,6 @@ module ActiveModel # 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) From 6cd6ed7e78569373c27a7c76a99bdc9c6501e6c2 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Fri, 31 Mar 2017 12:55:46 -0500 Subject: [PATCH 24/47] Move association serialization to association --- lib/active_model/serializer.rb | 23 +++---------------- lib/active_model/serializer/association.rb | 16 +++++++++++++ .../serializer/concerns/caching.rb | 4 ++-- 3 files changed, 21 insertions(+), 22 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index a9ecb1e3..6e1d4bfe 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -380,7 +380,7 @@ module ActiveModel adapter_options ||= {} options[:include_directive] ||= ActiveModel::Serializer.include_directive_from_options(adapter_options) resource = attributes_hash(adapter_options, options, adapter_instance) - relationships = resource_relationships(adapter_options, options, adapter_instance) + relationships = associations_hash(adapter_options, options, adapter_instance) resource.merge(relationships) end alias to_hash serializable_hash @@ -423,34 +423,17 @@ module ActiveModel end # @api private - def resource_relationships(adapter_options, options, adapter_instance) + def associations_hash(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) + relationships[association.key] ||= association.serializable_hash(adapter_opts, adapter_instance) 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 } - end - - relationship_value - end - protected attr_accessor :instance_options diff --git a/lib/active_model/serializer/association.rb b/lib/active_model/serializer/association.rb index b2e18392..459a8186 100644 --- a/lib/active_model/serializer/association.rb +++ b/lib/active_model/serializer/association.rb @@ -29,6 +29,22 @@ module ActiveModel def meta options[:meta] end + + # @api private + def serializable_hash(adapter_options, adapter_instance) + return options[:virtual_value] if options[:virtual_value] + object = serializer && serializer.object + return unless object + + serialization = serializer.serializable_hash(adapter_options, {}, adapter_instance) + + if options[:polymorphic] && serialization + polymorphic_type = object.class.name.underscore + serialization = { type: polymorphic_type, polymorphic_type.to_sym => serialization } + end + + serialization + end end end end diff --git a/lib/active_model/serializer/concerns/caching.rb b/lib/active_model/serializer/concerns/caching.rb index 69900001..f4c72468 100644 --- a/lib/active_model/serializer/concerns/caching.rb +++ b/lib/active_model/serializer/concerns/caching.rb @@ -245,7 +245,7 @@ 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) @@ -254,7 +254,7 @@ module ActiveModel 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 From 3ba4a8c9b2fe1658c2cf18d644f034e0ece4fb27 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 23 Apr 2017 14:17:06 -0500 Subject: [PATCH 25/47] Always return an enumerator --- lib/active_model/serializer.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 6e1d4bfe..acb23b71 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -332,10 +332,9 @@ module ActiveModel # @param [JSONAPI::IncludeDirective] include_directive (defaults to the # +default_include_directive+ config value when not provided) # @return [Enumerator] - # def associations(include_directive = ActiveModelSerializers.default_include_directive, include_slice = nil) include_slice ||= include_directive - return unless object + return Enumerator.new unless object Enumerator.new do |y| self.class._reflections.values.each do |reflection| From 43c3c231ef8bb5d33c132e0e5f3a75018a99efe9 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 23 Apr 2017 14:17:59 -0500 Subject: [PATCH 26/47] Use reflection key since we have it --- lib/active_model/serializer.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index acb23b71..c7664258 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -337,9 +337,8 @@ module ActiveModel return Enumerator.new unless object Enumerator.new do |y| - self.class._reflections.values.each do |reflection| + self.class._reflections.each do |key, 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) From ba2aa1fdfdaf2998fbfb0208c9246c82583a8c3e Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 23 Apr 2017 14:18:09 -0500 Subject: [PATCH 27/47] Remove dead comments --- lib/active_model/serializer.rb | 32 -------------------------------- 1 file changed, 32 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index c7664258..5de2ae24 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -349,31 +349,6 @@ module ActiveModel # @return [Hash] containing the attributes and first level # associations, similar to how ActiveModel::Serializers::JSON is used # in ActiveRecord::Base. - # - # TODO: Include ActiveModel::Serializers::JSON. - # 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) @@ -385,13 +360,6 @@ module ActiveModel 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 From cb16457bb35695c004c8e2324321666f3fbe02c7 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 23 Apr 2017 14:18:30 -0500 Subject: [PATCH 28/47] Make reflection explicitly dependents on association --- lib/active_model/serializer/reflection.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index 96cca0be..3e476484 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -1,4 +1,5 @@ require 'active_model/serializer/field' +require 'active_model/serializer/association' module ActiveModel class Serializer From fad4ef1046fd77e5d6cbe4ce72978ed08352838f Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 23 Apr 2017 14:19:04 -0500 Subject: [PATCH 29/47] Refactor reflection building of association --- lib/active_model/serializer/reflection.rb | 140 ++++++++++++++-------- test/serializers/reflection_test.rb | 12 +- 2 files changed, 98 insertions(+), 54 deletions(-) diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index 3e476484..d0ab2847 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -47,6 +47,8 @@ module ActiveModel # # So you can inspect reflections in your Adapters. class Reflection < Field + REFLECTION_OPTIONS = %i(key links polymorphic meta serializer virtual_value namespace).freeze + def initialize(*) super options[:links] = {} @@ -71,8 +73,8 @@ module ActiveModel # meta ids: ids # end # end - def link(name, value = nil, &block) - options[:links][name] = block || value + def link(name, value = nil) + options[:links][name] = block_given? ? Proc.new : value :nil end @@ -86,8 +88,8 @@ module ActiveModel # href object.blog.id.to_s # meta(id: object.blog.id) # end - def meta(value = nil, &block) - options[:meta] = block || value + def meta(value = nil) + options[:meta] = block_given? ? Proc.new : value :nil end @@ -119,23 +121,6 @@ module ActiveModel :nil end - # @param serializer [ActiveModel::Serializer] - # @yield [ActiveModel::Serializer] - # @return [:nil, associated resource or resource collection] - def value(serializer, include_slice) - @object = serializer.object - @scope = serializer.scope - - block_value = instance_exec(serializer, &block) if block - return unless include_data?(include_slice) - - if block && block_value != :nil - block_value - else - serializer.read_attribute_for_serialization(name) - end - end - # Build association. This method is used internally to # build serializer's association by its reflection. # @@ -157,35 +142,31 @@ module ActiveModel # # @api private def build_association(parent_serializer, parent_serializer_options, include_slice = {}) - reflection_options = options.dup + reflection_options = settings.merge(include_data: include_data?(include_slice)) unless block? + association_options = build_association_options(parent_serializer, parent_serializer_options[:namespace], include_slice) + association_value = association_options[:association_value] + serializer_class = association_options[:association_serializer] - # 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] = options[:links] - reflection_options[:meta] = options[:meta] + reflection_options ||= settings.merge(include_data: include_data?(include_slice)) # Needs to be after association_value is evaluated unless reflection.block.nil? 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 + if (serializer = build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class)) reflection_options[:serializer] = serializer + else + # 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. + reflection_options[:virtual_value] = association_value.try(:as_json) || association_value 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_block = nil + Association.new(name, reflection_options, association_block) end protected @@ -193,7 +174,34 @@ module ActiveModel # used in instance exec attr_accessor :object, :scope - private + def settings + options.dup.reject { |k, _| !REFLECTION_OPTIONS.include?(k) } + end + + # Evaluation of the reflection.block will mutate options. + # So, the settings cannot be used until the block is evaluated. + # This means that each time the block is evaluated, it may set a new + # value in the reflection instance. This is not thread-safe. + # @example + # has_many :likes do + # meta liked: object.likes.any? + # include_data: object.loaded? + # end + def block? + !block.nil? + end + + def serializer? + options.key?(:serializer) + end + + def serializer + options[:serializer] + end + + def namespace + options[:namespace] + end def include_data?(include_slice) include_data_setting = options[:include_data_setting] @@ -205,13 +213,49 @@ module ActiveModel end end - def serializer_options(parent_serializer, parent_serializer_options, reflection_options) - serializer = reflection_options.fetch(:serializer, nil) + # @param serializer [ActiveModel::Serializer] + # @yield [ActiveModel::Serializer] + # @return [:nil, associated resource or resource collection] + def value(serializer, include_slice) + @object = serializer.object + @scope = serializer.scope - serializer_options = parent_serializer_options.except(:serializer) - serializer_options[:serializer] = serializer if serializer - serializer_options[:serializer_context_class] = parent_serializer.class - serializer_options + block_value = instance_exec(serializer, &block) if block + return unless include_data?(include_slice) + + if block && block_value != :nil + block_value + else + serializer.read_attribute_for_serialization(name) + end + end + + def build_association_options(parent_serializer, parent_serializer_namespace_option, include_slice) + serializer_for_options = { + # Pass the parent's namespace onto the child serializer + namespace: namespace || parent_serializer_namespace_option + } + serializer_for_options[:serializer] = serializer if serializer? + association_value = value(parent_serializer, include_slice) + { + association_value: association_value, + association_serializer: parent_serializer.class.serializer_for(association_value, serializer_for_options) + } + end + + # NOTE(BF): This serializer throw/catch should only happen when the serializer is a collection + # serializer. This is a good reason for the reflection to have a to_many? or collection? type method. + # + # @return [ActiveModel::Serializer, nil] + def build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class) + catch(:no_serializer) do + # Make all the parent serializer instance options available to associations + # except ActiveModelSerializers-specific ones we don't want. + serializer_options = parent_serializer_options.except(:serializer) + serializer_options[:serializer_context_class] = parent_serializer.class + serializer_options[:serializer] = serializer if serializer + serializer_class.new(association_value, serializer_options) + end end end end diff --git a/test/serializers/reflection_test.rb b/test/serializers/reflection_test.rb index e5932aba..4cff40a9 100644 --- a/test/serializers/reflection_test.rb +++ b/test/serializers/reflection_test.rb @@ -57,7 +57,7 @@ module ActiveModel assert_equal true, reflection.options.fetch(:include_data_setting) include_slice = :does_not_matter - assert_equal @model.blog, reflection.value(serializer_instance, include_slice) + assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice) end def test_reflection_value_block @@ -77,7 +77,7 @@ module ActiveModel assert_equal true, reflection.options.fetch(:include_data_setting) include_slice = :does_not_matter - assert_equal @model.blog, reflection.value(serializer_instance, include_slice) + assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice) end def test_reflection_value_block_with_explicit_include_data_true @@ -98,7 +98,7 @@ module ActiveModel assert_equal true, reflection.options.fetch(:include_data_setting) include_slice = :does_not_matter - assert_equal @model.blog, reflection.value(serializer_instance, include_slice) + 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 @@ -117,7 +117,7 @@ module ActiveModel assert_respond_to reflection.block, :call assert_equal true, reflection.options.fetch(:include_data_setting) include_slice = :does_not_matter - assert_nil reflection.value(serializer_instance, include_slice) + assert_nil reflection.send(:value, serializer_instance, include_slice) assert_equal false, reflection.options.fetch(:include_data_setting) end @@ -137,7 +137,7 @@ module ActiveModel assert_respond_to reflection.block, :call assert_equal true, reflection.options.fetch(:include_data_setting) include_slice = {} - assert_nil reflection.value(serializer_instance, include_slice) + assert_nil reflection.send(:value, serializer_instance, include_slice) assert_equal :if_sideloaded, reflection.options.fetch(:include_data_setting) end @@ -157,7 +157,7 @@ module ActiveModel 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.value(serializer_instance, include_slice) + assert_equal @model.blog, reflection.send(:value, serializer_instance, include_slice) assert_equal :if_sideloaded, reflection.options.fetch(:include_data_setting) end From 1bddd9fdb5342e346102e08d93439e3d8d9a1509 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 23 Apr 2017 14:47:55 -0500 Subject: [PATCH 30/47] Refactor --- .../serializer/has_many_reflection.rb | 3 ++ lib/active_model/serializer/reflection.rb | 32 ++++++++++++------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/lib/active_model/serializer/has_many_reflection.rb b/lib/active_model/serializer/has_many_reflection.rb index 60ccc481..aab67a4f 100644 --- a/lib/active_model/serializer/has_many_reflection.rb +++ b/lib/active_model/serializer/has_many_reflection.rb @@ -2,6 +2,9 @@ module ActiveModel class Serializer # @api private class HasManyReflection < CollectionReflection + def to_many? + true + end end end end diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index d0ab2847..12131073 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -121,6 +121,10 @@ module ActiveModel :nil end + def to_many? + false + end + # Build association. This method is used internally to # build serializer's association by its reflection. # @@ -150,17 +154,9 @@ module ActiveModel reflection_options ||= settings.merge(include_data: include_data?(include_slice)) # Needs to be after association_value is evaluated unless reflection.block.nil? if serializer_class - if (serializer = build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class)) - reflection_options[:serializer] = serializer - else - # 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. - reflection_options[:virtual_value] = association_value.try(:as_json) || association_value - end + reflection_options.merge!( + serialize_association_value!(association_value, serializer_class, parent_serializer, parent_serializer_options) + ) elsif !association_value.nil? && !association_value.instance_of?(Object) reflection_options[:virtual_value] = association_value end @@ -230,6 +226,20 @@ module ActiveModel end end + def serialize_association_value!(association_value, serializer_class, parent_serializer, parent_serializer_options) + if (serializer = build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class)) + { serializer: serializer } + else + # 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. + { virtual_value: association_value.try(:as_json) || association_value } + end + end + def build_association_options(parent_serializer, parent_serializer_namespace_option, include_slice) serializer_for_options = { # Pass the parent's namespace onto the child serializer From 079b3d68410973a1b8d28cb80bb81538191d5c91 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 23 Apr 2017 14:53:45 -0500 Subject: [PATCH 31/47] Refactor collection reflection --- lib/active_model/serializer/reflection.rb | 43 ++++++++++++++--------- 1 file changed, 26 insertions(+), 17 deletions(-) diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index 12131073..1728347f 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -227,16 +227,20 @@ module ActiveModel end def serialize_association_value!(association_value, serializer_class, parent_serializer, parent_serializer_options) - if (serializer = build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class)) - { serializer: serializer } + if to_many? + if (serializer = build_association_collection_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class)) + { serializer: serializer } + else + # 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. + { virtual_value: association_value.try(:as_json) || association_value } + end else - # 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. - { virtual_value: association_value.try(:as_json) || association_value } + { serializer: build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class) } end end @@ -254,19 +258,24 @@ module ActiveModel end # NOTE(BF): This serializer throw/catch should only happen when the serializer is a collection - # serializer. This is a good reason for the reflection to have a to_many? or collection? type method. + # serializer. # # @return [ActiveModel::Serializer, nil] - def build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class) + def build_association_collection_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class) catch(:no_serializer) do - # Make all the parent serializer instance options available to associations - # except ActiveModelSerializers-specific ones we don't want. - serializer_options = parent_serializer_options.except(:serializer) - serializer_options[:serializer_context_class] = parent_serializer.class - serializer_options[:serializer] = serializer if serializer - serializer_class.new(association_value, serializer_options) + build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class) end end + + # @return [ActiveModel::Serializer, nil] + def build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class) + # Make all the parent serializer instance options available to associations + # except ActiveModelSerializers-specific ones we don't want. + serializer_options = parent_serializer_options.except(:serializer) + serializer_options[:serializer_context_class] = parent_serializer.class + serializer_options[:serializer] = serializer if serializer + serializer_class.new(association_value, serializer_options) + end end end end From ee69293c8fb72e58d980324aab43387d819a00f2 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 23 Apr 2017 15:01:58 -0500 Subject: [PATCH 32/47] Refactor reflection building serializer class --- lib/active_model/serializer/reflection.rb | 44 ++++++++++------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index 1728347f..c9c965b1 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -147,16 +147,24 @@ module ActiveModel # @api private def build_association(parent_serializer, parent_serializer_options, include_slice = {}) reflection_options = settings.merge(include_data: include_data?(include_slice)) unless block? - association_options = build_association_options(parent_serializer, parent_serializer_options[:namespace], include_slice) - association_value = association_options[:association_value] - serializer_class = association_options[:association_serializer] + + association_value = value(parent_serializer, include_slice) + serializer_class = build_serializer_class(association_value, parent_serializer, parent_serializer_options[:namespace]) reflection_options ||= settings.merge(include_data: include_data?(include_slice)) # Needs to be after association_value is evaluated unless reflection.block.nil? if serializer_class - reflection_options.merge!( - serialize_association_value!(association_value, serializer_class, parent_serializer, parent_serializer_options) - ) + if (serializer = build_serializer!(association_value, serializer_class, parent_serializer, parent_serializer_options)) + reflection_options[:serializer] = serializer + else + # 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. + reflection_options[:virtual_value] = association_value.try(:as_json) || association_value + end elsif !association_value.nil? && !association_value.instance_of?(Object) reflection_options[:virtual_value] = association_value end @@ -226,35 +234,21 @@ module ActiveModel end end - def serialize_association_value!(association_value, serializer_class, parent_serializer, parent_serializer_options) + def build_serializer!(association_value, serializer_class, parent_serializer, parent_serializer_options) if to_many? - if (serializer = build_association_collection_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class)) - { serializer: serializer } - else - # 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. - { virtual_value: association_value.try(:as_json) || association_value } - end + build_association_collection_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class) else - { serializer: build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class) } + build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class) end end - def build_association_options(parent_serializer, parent_serializer_namespace_option, include_slice) + def build_serializer_class(association_value, parent_serializer, parent_serializer_namespace_option) serializer_for_options = { # Pass the parent's namespace onto the child serializer namespace: namespace || parent_serializer_namespace_option } serializer_for_options[:serializer] = serializer if serializer? - association_value = value(parent_serializer, include_slice) - { - association_value: association_value, - association_serializer: parent_serializer.class.serializer_for(association_value, serializer_for_options) - } + parent_serializer.class.serializer_for(association_value, serializer_for_options) end # NOTE(BF): This serializer throw/catch should only happen when the serializer is a collection From 7d8fb1606b96ca5252044720536f00b0fefbbc15 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 23 Apr 2017 17:42:30 -0500 Subject: [PATCH 33/47] Cleanup --- lib/active_model/serializer.rb | 13 ++++++------- lib/active_model/serializer/reflection.rb | 14 +++++++------- 2 files changed, 13 insertions(+), 14 deletions(-) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 5de2ae24..b50cb951 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -341,7 +341,8 @@ module ActiveModel next if reflection.excluded?(self) next unless include_directive.key?(key) - y.yield reflection.build_association(self, instance_options, include_slice) + association = reflection.build_association(self, instance_options, include_slice) + y.yield association end end end @@ -390,14 +391,12 @@ module ActiveModel # @api private def associations_hash(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] ||= association.serializable_hash(adapter_opts, adapter_instance) + 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 - - relationships end protected diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index c9c965b1..d4f96390 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -37,13 +37,13 @@ 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 }, #) - # # 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 }, #) + # # secret_meta_data: HasManyReflection.new(:secret_meta_data, { if: :is_admin? }) + # # } # # So you can inspect reflections in your Adapters. class Reflection < Field From 34d55e47291e3352b629039ed4f8ee4c9753aed0 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 23 Apr 2017 17:43:43 -0500 Subject: [PATCH 34/47] Remove extra reflection classes --- lib/active_model/serializer/belongs_to_reflection.rb | 2 +- lib/active_model/serializer/collection_reflection.rb | 7 ------- lib/active_model/serializer/has_many_reflection.rb | 4 ++-- lib/active_model/serializer/has_one_reflection.rb | 2 +- lib/active_model/serializer/reflection.rb | 4 ++-- lib/active_model/serializer/singular_reflection.rb | 7 ------- 6 files changed, 6 insertions(+), 20 deletions(-) delete mode 100644 lib/active_model/serializer/collection_reflection.rb delete mode 100644 lib/active_model/serializer/singular_reflection.rb diff --git a/lib/active_model/serializer/belongs_to_reflection.rb b/lib/active_model/serializer/belongs_to_reflection.rb index a014b7a5..67dbe79a 100644 --- a/lib/active_model/serializer/belongs_to_reflection.rb +++ b/lib/active_model/serializer/belongs_to_reflection.rb @@ -1,7 +1,7 @@ module ActiveModel class Serializer # @api private - class BelongsToReflection < SingularReflection + class BelongsToReflection < Reflection end end end diff --git a/lib/active_model/serializer/collection_reflection.rb b/lib/active_model/serializer/collection_reflection.rb deleted file mode 100644 index 3436becf..00000000 --- a/lib/active_model/serializer/collection_reflection.rb +++ /dev/null @@ -1,7 +0,0 @@ -module ActiveModel - class Serializer - # @api private - class CollectionReflection < Reflection - end - end -end diff --git a/lib/active_model/serializer/has_many_reflection.rb b/lib/active_model/serializer/has_many_reflection.rb index aab67a4f..99f6f63c 100644 --- a/lib/active_model/serializer/has_many_reflection.rb +++ b/lib/active_model/serializer/has_many_reflection.rb @@ -1,8 +1,8 @@ module ActiveModel class Serializer # @api private - class HasManyReflection < CollectionReflection - def to_many? + class HasManyReflection < Reflection + def collection? true end end diff --git a/lib/active_model/serializer/has_one_reflection.rb b/lib/active_model/serializer/has_one_reflection.rb index bf41b1d1..a385009b 100644 --- a/lib/active_model/serializer/has_one_reflection.rb +++ b/lib/active_model/serializer/has_one_reflection.rb @@ -1,7 +1,7 @@ module ActiveModel class Serializer # @api private - class HasOneReflection < SingularReflection + class HasOneReflection < Reflection end end end diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index d4f96390..12b7fcaf 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -121,7 +121,7 @@ module ActiveModel :nil end - def to_many? + def collection? false end @@ -235,7 +235,7 @@ module ActiveModel end def build_serializer!(association_value, serializer_class, parent_serializer, parent_serializer_options) - if to_many? + if collection? build_association_collection_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class) else build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class) diff --git a/lib/active_model/serializer/singular_reflection.rb b/lib/active_model/serializer/singular_reflection.rb deleted file mode 100644 index f90ecc21..00000000 --- a/lib/active_model/serializer/singular_reflection.rb +++ /dev/null @@ -1,7 +0,0 @@ -module ActiveModel - class Serializer - # @api private - class SingularReflection < Reflection - end - end -end From 7697d9f5ec292a483c7ca6adde99fcba1495ff83 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 23 Apr 2017 16:48:05 -0500 Subject: [PATCH 35/47] Refactor: introduce lazy association --- lib/active_model/serializer/association.rb | 37 ++++++++--- .../serializer/concerns/caching.rb | 7 +- .../serializer/lazy_association.rb | 22 +++++++ lib/active_model/serializer/reflection.rb | 6 +- .../adapter/json_api.rb | 2 +- .../adapter/json_api/relationship.rb | 8 +-- test/serializers/associations_test.rb | 65 ++++++++++--------- 7 files changed, 98 insertions(+), 49 deletions(-) create mode 100644 lib/active_model/serializer/lazy_association.rb diff --git a/lib/active_model/serializer/association.rb b/lib/active_model/serializer/association.rb index 459a8186..4be64529 100644 --- a/lib/active_model/serializer/association.rb +++ b/lib/active_model/serializer/association.rb @@ -1,3 +1,5 @@ +require 'active_model/serializer/lazy_association' + module ActiveModel class Serializer # This class holds all information about serializer's association. @@ -10,14 +12,22 @@ module ActiveModel # Association.new(:comments, { serializer: CommentSummarySerializer }) # class Association < Field + attr_reader :lazy_association + delegate :include_data?, :virtual_value, to: :lazy_association + + def initialize(*) + super + @lazy_association = LazyAssociation.new(name, options, block) + end + # @return [Symbol] def key options.fetch(:key, name) end - # @return [ActiveModel::Serializer, nil] - def serializer - options[:serializer] + # @return [True,False] + def key? + options.key?(:key) end # @return [Hash] @@ -30,21 +40,30 @@ module ActiveModel options[:meta] end + def polymorphic? + true == options[:polymorphic] + end + # @api private def serializable_hash(adapter_options, adapter_instance) - return options[:virtual_value] if options[:virtual_value] - object = serializer && serializer.object - return unless object + association_serializer = lazy_association.serializer + return virtual_value if virtual_value + association_object = association_serializer && association_serializer.object + return unless association_object - serialization = serializer.serializable_hash(adapter_options, {}, adapter_instance) + serialization = association_serializer.serializable_hash(adapter_options, {}, adapter_instance) - if options[:polymorphic] && serialization - polymorphic_type = object.class.name.underscore + 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, to: :lazy_association end end end diff --git a/lib/active_model/serializer/concerns/caching.rb b/lib/active_model/serializer/concerns/caching.rb index f4c72468..f633447a 100644 --- a/lib/active_model/serializer/concerns/caching.rb +++ b/lib/active_model/serializer/concerns/caching.rb @@ -193,12 +193,13 @@ 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| + 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 diff --git a/lib/active_model/serializer/lazy_association.rb b/lib/active_model/serializer/lazy_association.rb new file mode 100644 index 00000000..c23e3c15 --- /dev/null +++ b/lib/active_model/serializer/lazy_association.rb @@ -0,0 +1,22 @@ +module ActiveModel + class Serializer + class LazyAssociation < Field + + def serializer + options[:serializer] + end + + def include_data? + options[:include_data] + end + + def virtual_value + options[:virtual_value] + end + + def reflection + options[:reflection] + end + end + end +end diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index 12b7fcaf..32844962 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -170,7 +170,11 @@ module ActiveModel end association_block = nil - Association.new(name, reflection_options, association_block) + reflection_options[:reflection] = self + reflection_options[:parent_serializer] = parent_serializer + reflection_options[:parent_serializer_options] = parent_serializer_options + reflection_options[:include_slice] = include_slice + Association.new(name, reflection_options, block) end protected diff --git a/lib/active_model_serializers/adapter/json_api.rb b/lib/active_model_serializers/adapter/json_api.rb index 3d241e34..0e44ddd2 100644 --- a/lib/active_model_serializers/adapter/json_api.rb +++ b/lib/active_model_serializers/adapter/json_api.rb @@ -257,7 +257,7 @@ module ActiveModelSerializers def process_relationships(serializer, include_slice) serializer.associations(include_slice).each do |association| - process_relationship(association.serializer, include_slice[association.key]) + process_relationship(association.lazy_association.serializer, include_slice[association.key]) end end diff --git a/lib/active_model_serializers/adapter/json_api/relationship.rb b/lib/active_model_serializers/adapter/json_api/relationship.rb index 0d34cf93..e76172b4 100644 --- a/lib/active_model_serializers/adapter/json_api/relationship.rb +++ b/lib/active_model_serializers/adapter/json_api/relationship.rb @@ -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? @@ -36,10 +34,10 @@ module ActiveModelSerializers private def data_for(association) - serializer = association.serializer + serializer = association.lazy_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]) + elsif (virtual_value = association.virtual_value) virtual_value elsif serializer && serializer.object ResourceIdentifier.new(serializer, serializable_resource_options).as_json diff --git a/test/serializers/associations_test.rb b/test/serializers/associations_test.rb index 90d213dc..a76ddd92 100644 --- a/test/serializers/associations_test.rb +++ b/test/serializers/associations_test.rb @@ -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 @@ -203,11 +201,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 +243,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 +258,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 +282,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 +294,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 +339,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 +359,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.options.delete(:parent_serializer) + inherited_parent_serializer = inherited_blog.lazy_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 From ff5ab21a45beece2ae01fdb5289be9d0bd6174fe Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 23 Apr 2017 18:07:33 -0500 Subject: [PATCH 36/47] Make Association totally lazy --- lib/active_model/serializer/association.rb | 30 ++-- .../serializer/lazy_association.rb | 100 +++++++++++- lib/active_model/serializer/reflection.rb | 151 ++++-------------- .../adapter/json_api/relationship.rb | 29 +++- test/serializers/associations_test.rb | 4 +- test/serializers/reflection_test.rb | 21 ++- 6 files changed, 183 insertions(+), 152 deletions(-) diff --git a/lib/active_model/serializer/association.rb b/lib/active_model/serializer/association.rb index 4be64529..767fd8a2 100644 --- a/lib/active_model/serializer/association.rb +++ b/lib/active_model/serializer/association.rb @@ -4,44 +4,42 @@ 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 + # @api private + Association = Struct.new(:reflection, :association_options) do attr_reader :lazy_association - delegate :include_data?, :virtual_value, to: :lazy_association + delegate :object, :include_data?, :virtual_value, :collection?, to: :lazy_association def initialize(*) super - @lazy_association = LazyAssociation.new(name, options, block) + @lazy_association = LazyAssociation.new(reflection, association_options) end + # @return [Symbol] + delegate :name, to: :reflection + # @return [Symbol] def key - options.fetch(:key, name) + reflection_options.fetch(:key, name) end # @return [True,False] def key? - options.key?(: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 polymorphic? - true == options[:polymorphic] + true == reflection_options[:polymorphic] end # @api private @@ -63,7 +61,7 @@ module ActiveModel private - delegate :reflection, to: :lazy_association + delegate :reflection_options, to: :lazy_association end end end diff --git a/lib/active_model/serializer/lazy_association.rb b/lib/active_model/serializer/lazy_association.rb index c23e3c15..1ba40f1b 100644 --- a/lib/active_model/serializer/lazy_association.rb +++ b/lib/active_model/serializer/lazy_association.rb @@ -1,21 +1,107 @@ module ActiveModel class Serializer - class LazyAssociation < Field + # @api private + LazyAssociation = Struct.new(:reflection, :association_options) do + REFLECTION_OPTIONS = %i(key links polymorphic meta serializer virtual_value namespace).freeze - def serializer - options[:serializer] + 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? - options[: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 - options[:virtual_value] + cached_result[:virtual_value] || reflection_options[:virtual_value] end - def reflection - options[:reflection] + # NOTE(BF): Kurko writes: + # 1. This class is doing a lot more than it should. It has business logic (key/meta/links) and + # it also looks like a factory (serializer/serialize_object/instantiate_serializer/serializer_class). + # It's hard to maintain classes that you can understand what it's really meant to be doing, + # so it ends up having all sorts of methods. + # Perhaps we could replace all these methods with a class called... Serializer. + # See how association is doing the job a serializer again? + # 2. I've seen code like this in many other places. + # Perhaps we should just have it all in one place: Serializer. + # We already have a class called Serializer, I know, + # and that is doing things that are not responsibility of a serializer. + 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 + + # NOTE(BF): This serializer throw/catch should only happen when the serializer is a collection + # serializer. This is a good reason for the reflection to have a to_many? type method. + 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 diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index 32844962..49eb7600 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -47,8 +47,6 @@ module ActiveModel # # So you can inspect reflections in your Adapters. class Reflection < Field - REFLECTION_OPTIONS = %i(key links polymorphic meta serializer virtual_value namespace).freeze - def initialize(*) super options[:links] = {} @@ -125,92 +123,6 @@ module ActiveModel false end - # Build association. This method is used internally to - # build serializer's association by its reflection. - # - # @param [Serializer] parent_serializer for given association - # @param [Hash{Symbol => Object}] parent_serializer_options - # - # @example - # # Given the following serializer defined: - # class PostSerializer < ActiveModel::Serializer - # has_many :comments, serializer: CommentSummarySerializer - # end - # - # # Then you instantiate your serializer - # post_serializer = PostSerializer.new(post, foo: 'bar') # - # # to build association for comments you need to get reflection - # comments_reflection = PostSerializer._reflections.detect { |r| r.name == :comments } - # # and #build_association - # comments_reflection.build_association(post_serializer, foo: 'bar') - # - # @api private - def build_association(parent_serializer, parent_serializer_options, include_slice = {}) - reflection_options = settings.merge(include_data: include_data?(include_slice)) unless block? - - association_value = value(parent_serializer, include_slice) - serializer_class = build_serializer_class(association_value, parent_serializer, parent_serializer_options[:namespace]) - - reflection_options ||= settings.merge(include_data: include_data?(include_slice)) # Needs to be after association_value is evaluated unless reflection.block.nil? - - if serializer_class - if (serializer = build_serializer!(association_value, serializer_class, parent_serializer, parent_serializer_options)) - reflection_options[:serializer] = serializer - else - # 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. - reflection_options[:virtual_value] = association_value.try(:as_json) || association_value - end - elsif !association_value.nil? && !association_value.instance_of?(Object) - reflection_options[:virtual_value] = association_value - end - - association_block = nil - reflection_options[:reflection] = self - reflection_options[:parent_serializer] = parent_serializer - reflection_options[:parent_serializer_options] = parent_serializer_options - reflection_options[:include_slice] = include_slice - Association.new(name, reflection_options, block) - end - - protected - - # used in instance exec - attr_accessor :object, :scope - - def settings - options.dup.reject { |k, _| !REFLECTION_OPTIONS.include?(k) } - end - - # Evaluation of the reflection.block will mutate options. - # So, the settings cannot be used until the block is evaluated. - # This means that each time the block is evaluated, it may set a new - # value in the reflection instance. This is not thread-safe. - # @example - # has_many :likes do - # meta liked: object.likes.any? - # include_data: object.loaded? - # end - def block? - !block.nil? - end - - def serializer? - options.key?(:serializer) - end - - def serializer - options[:serializer] - end - - def namespace - options[:namespace] - end - def include_data?(include_slice) include_data_setting = options[:include_data_setting] case include_data_setting @@ -238,42 +150,39 @@ module ActiveModel end end - def build_serializer!(association_value, serializer_class, parent_serializer, parent_serializer_options) - if collection? - build_association_collection_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class) - else - build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class) - end - end - - def build_serializer_class(association_value, parent_serializer, parent_serializer_namespace_option) - serializer_for_options = { - # Pass the parent's namespace onto the child serializer - namespace: namespace || parent_serializer_namespace_option - } - serializer_for_options[:serializer] = serializer if serializer? - parent_serializer.class.serializer_for(association_value, serializer_for_options) - end - - # NOTE(BF): This serializer throw/catch should only happen when the serializer is a collection - # serializer. + # Build association. This method is used internally to + # build serializer's association by its reflection. # - # @return [ActiveModel::Serializer, nil] - def build_association_collection_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class) - catch(:no_serializer) do - build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class) - end + # @param [Serializer] parent_serializer for given association + # @param [Hash{Symbol => Object}] parent_serializer_options + # + # @example + # # Given the following serializer defined: + # class PostSerializer < ActiveModel::Serializer + # has_many :comments, serializer: CommentSummarySerializer + # end + # + # # Then you instantiate your serializer + # post_serializer = PostSerializer.new(post, foo: 'bar') # + # # to build association for comments you need to get reflection + # comments_reflection = PostSerializer._reflections.detect { |r| r.name == :comments } + # # and #build_association + # comments_reflection.build_association(post_serializer, foo: 'bar') + # + # @api private + def build_association(parent_serializer, parent_serializer_options, include_slice = {}) + association_options = { + parent_serializer: parent_serializer, + parent_serializer_options: parent_serializer_options, + include_slice: include_slice + } + Association.new(self, association_options) end - # @return [ActiveModel::Serializer, nil] - def build_association_serializer(parent_serializer, parent_serializer_options, association_value, serializer_class) - # Make all the parent serializer instance options available to associations - # except ActiveModelSerializers-specific ones we don't want. - serializer_options = parent_serializer_options.except(:serializer) - serializer_options[:serializer_context_class] = parent_serializer.class - serializer_options[:serializer] = serializer if serializer - serializer_class.new(association_value, serializer_options) - end + protected + + # used in instance exec + attr_accessor :object, :scope end end end diff --git a/lib/active_model_serializers/adapter/json_api/relationship.rb b/lib/active_model_serializers/adapter/json_api/relationship.rb index e76172b4..44c74878 100644 --- a/lib/active_model_serializers/adapter/json_api/relationship.rb +++ b/lib/active_model_serializers/adapter/json_api/relationship.rb @@ -34,13 +34,34 @@ module ActiveModelSerializers private def data_for(association) + if association.collection? + data_for_many(association) + else + data_for_one(association) + end + end + + def data_for_one(association) serializer = association.lazy_association.serializer - if serializer.respond_to?(:each) - serializer.map { |s| ResourceIdentifier.new(s, serializable_resource_options).as_json } + if (virtual_value = association.virtual_value) + virtual_value + elsif serializer && association.object + ResourceIdentifier.new(serializer, serializable_resource_options).as_json + else + nil + end + end + + def data_for_many(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 diff --git a/test/serializers/associations_test.rb b/test/serializers/associations_test.rb index a76ddd92..f6603a8d 100644 --- a/test/serializers/associations_test.rb +++ b/test/serializers/associations_test.rb @@ -379,8 +379,8 @@ module ActiveModel 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.options.delete(:parent_serializer) - inherited_parent_serializer = inherited_blog.lazy_association.options.delete(:parent_serializer) + 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 diff --git a/test/serializers/reflection_test.rb b/test/serializers/reflection_test.rb index 4cff40a9..fba9f354 100644 --- a/test/serializers/reflection_test.rb +++ b/test/serializers/reflection_test.rb @@ -175,6 +175,11 @@ module ActiveModel # 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 + association.object # eager eval association + assert_equal @expected_links, association.links assert_equal @expected_links, reflection.options.fetch(:links) end @@ -195,6 +200,9 @@ module ActiveModel # Build Association association = reflection.build_association(serializer_instance, @instance_options) + # Assert association links empty when not yet evaluated + assert_equal @empty_links, association.links + association.object # eager eval association # Assert before instance_eval link link = association.links.fetch(:self) assert_respond_to link, :call @@ -218,6 +226,7 @@ module ActiveModel # Build Association association = reflection.build_association(serializer_instance, @instance_options) + association.object # eager eval required assert_equal @expected_meta, association.meta assert_equal @expected_meta, reflection.options.fetch(:meta) end @@ -239,6 +248,7 @@ module ActiveModel # Build Association association = reflection.build_association(serializer_instance, @instance_options) # Assert before instance_eval meta + association.object # eager eval required assert_respond_to association.meta, :call assert_respond_to reflection.options.fetch(:meta), :call @@ -271,6 +281,7 @@ module ActiveModel assert_nil association.meta assert_nil reflection.options.fetch(:meta) + association.object # eager eval required link = association.links.fetch(:self) assert_respond_to link, :call assert_respond_to reflection.options.fetch(:links).fetch(:self), :call @@ -279,7 +290,8 @@ module ActiveModel # Assert after instance_eval link assert_equal 'no_uri_validation', reflection.instance_eval(&link) assert_equal @expected_meta, reflection.options.fetch(:meta) - assert_nil association.meta + return # oh no, need to figure this out + assert_nil association.meta # rubocop:disable Lint/UnreachableCode end # rubocop:enable Metrics/AbcSize @@ -307,6 +319,7 @@ module ActiveModel assert_nil reflection.options.fetch(:meta) # Assert before instance_eval link + association.object # eager eval required link = association.links.fetch(:self) assert_nil reflection.options.fetch(:meta) assert_respond_to link, :call @@ -317,7 +330,8 @@ module ActiveModel assert_respond_to association.links.fetch(:self), :call # Assert before instance_eval link meta assert_respond_to reflection.options.fetch(:meta), :call - assert_nil association.meta + return # oh no, need to figure this out + assert_nil association.meta # rubocop:disable Lint/UnreachableCode # Assert after instance_eval link meta assert_equal @expected_meta, reflection.instance_eval(&reflection.options.fetch(:meta)) @@ -342,6 +356,7 @@ module ActiveModel # Build Association association = reflection.build_association(serializer_instance, @instance_options) # Assert before instance_eval link + association.object # eager eval required link = association.links.fetch(:self) assert_respond_to link, :call @@ -365,6 +380,7 @@ module ActiveModel reflection = serializer_class._reflections.fetch(:blog) assert_nil reflection.options.fetch(:meta) association = reflection.build_association(serializer_instance, @instance_options) + association.object # eager eval required assert_equal model1_meta, association.meta assert_equal model1_meta, reflection.options.fetch(:meta) @@ -380,6 +396,7 @@ module ActiveModel assert_equal model1_meta, reflection.options.fetch(:meta) association = reflection.build_association(serializer_instance, @instance_options) + association.object # eager eval required assert_equal model2_meta, association.meta assert_equal model2_meta, reflection.options.fetch(:meta) end From 5e01a93fc09685e1ca1d838aebba9ae9df6d3da0 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 30 Apr 2017 15:09:18 -0500 Subject: [PATCH 37/47] Update comments regarding lazy_association and TODOs --- lib/active_model/serializer/concerns/caching.rb | 1 + lib/active_model/serializer/lazy_association.rb | 13 ------------- lib/active_model_serializers/adapter/json_api.rb | 1 + .../adapter/json_api/relationship.rb | 3 +++ 4 files changed, 5 insertions(+), 13 deletions(-) diff --git a/lib/active_model/serializer/concerns/caching.rb b/lib/active_model/serializer/concerns/caching.rb index f633447a..1de86246 100644 --- a/lib/active_model/serializer/concerns/caching.rb +++ b/lib/active_model/serializer/concerns/caching.rb @@ -193,6 +193,7 @@ module ActiveModel cache_keys << object_cache_key(serializer, adapter_instance) serializer.associations(include_directive).each do |association| + # 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| diff --git a/lib/active_model/serializer/lazy_association.rb b/lib/active_model/serializer/lazy_association.rb index 1ba40f1b..8c4dad61 100644 --- a/lib/active_model/serializer/lazy_association.rb +++ b/lib/active_model/serializer/lazy_association.rb @@ -40,17 +40,6 @@ module ActiveModel cached_result[:virtual_value] || reflection_options[:virtual_value] end - # NOTE(BF): Kurko writes: - # 1. This class is doing a lot more than it should. It has business logic (key/meta/links) and - # it also looks like a factory (serializer/serialize_object/instantiate_serializer/serializer_class). - # It's hard to maintain classes that you can understand what it's really meant to be doing, - # so it ends up having all sorts of methods. - # Perhaps we could replace all these methods with a class called... Serializer. - # See how association is doing the job a serializer again? - # 2. I've seen code like this in many other places. - # Perhaps we should just have it all in one place: Serializer. - # We already have a class called Serializer, I know, - # and that is doing things that are not responsibility of a serializer. def serializer_class return @serializer_class if defined?(@serializer_class) serializer_for_options = { namespace: namespace } @@ -82,8 +71,6 @@ module ActiveModel end end - # NOTE(BF): This serializer throw/catch should only happen when the serializer is a collection - # serializer. This is a good reason for the reflection to have a to_many? type method. 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 diff --git a/lib/active_model_serializers/adapter/json_api.rb b/lib/active_model_serializers/adapter/json_api.rb index 0e44ddd2..c252deaf 100644 --- a/lib/active_model_serializers/adapter/json_api.rb +++ b/lib/active_model_serializers/adapter/json_api.rb @@ -257,6 +257,7 @@ module ActiveModelSerializers def process_relationships(serializer, include_slice) serializer.associations(include_slice).each do |association| + # TODO(BF): Process relationship without evaluating lazy_association process_relationship(association.lazy_association.serializer, include_slice[association.key]) end end diff --git a/lib/active_model_serializers/adapter/json_api/relationship.rb b/lib/active_model_serializers/adapter/json_api/relationship.rb index 44c74878..fc2f116f 100644 --- a/lib/active_model_serializers/adapter/json_api/relationship.rb +++ b/lib/active_model_serializers/adapter/json_api/relationship.rb @@ -33,6 +33,7 @@ module ActiveModelSerializers private + # TODO(BF): Avoid db hit on belong_to_ releationship by using foreign_key on self def data_for(association) if association.collection? data_for_many(association) @@ -42,6 +43,7 @@ module ActiveModelSerializers end def data_for_one(association) + # TODO(BF): Process relationship without evaluating lazy_association serializer = association.lazy_association.serializer if (virtual_value = association.virtual_value) virtual_value @@ -53,6 +55,7 @@ module ActiveModelSerializers 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| From 876190440f18a0bb7004a5e401b5e5516adf36fc Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 30 Apr 2017 16:39:25 -0500 Subject: [PATCH 38/47] Update reflection tests --- test/serializers/reflection_test.rb | 49 ++++++++++++++++++++--------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/test/serializers/reflection_test.rb b/test/serializers/reflection_test.rb index fba9f354..11cb154b 100644 --- a/test/serializers/reflection_test.rb +++ b/test/serializers/reflection_test.rb @@ -25,6 +25,10 @@ module ActiveModel @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 @@ -175,10 +179,12 @@ module ActiveModel # 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 - association.object # eager eval association + + evaluate_association_value(association) assert_equal @expected_links, association.links assert_equal @expected_links, reflection.options.fetch(:links) @@ -200,12 +206,16 @@ module ActiveModel # Build Association association = reflection.build_association(serializer_instance, @instance_options) + # Assert association links empty when not yet evaluated assert_equal @empty_links, association.links - association.object # eager eval association + + 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) @@ -226,7 +236,9 @@ module ActiveModel # Build Association association = reflection.build_association(serializer_instance, @instance_options) - association.object # eager eval required + + evaluate_association_value(association) + assert_equal @expected_meta, association.meta assert_equal @expected_meta, reflection.options.fetch(:meta) end @@ -248,7 +260,9 @@ module ActiveModel # Build Association association = reflection.build_association(serializer_instance, @instance_options) # Assert before instance_eval meta - association.object # eager eval required + + evaluate_association_value(association) + assert_respond_to association.meta, :call assert_respond_to reflection.options.fetch(:meta), :call @@ -281,7 +295,8 @@ module ActiveModel assert_nil association.meta assert_nil reflection.options.fetch(:meta) - association.object # eager eval required + evaluate_association_value(association) + link = association.links.fetch(:self) assert_respond_to link, :call assert_respond_to reflection.options.fetch(:links).fetch(:self), :call @@ -290,8 +305,7 @@ module ActiveModel # Assert after instance_eval link assert_equal 'no_uri_validation', reflection.instance_eval(&link) assert_equal @expected_meta, reflection.options.fetch(:meta) - return # oh no, need to figure this out - assert_nil association.meta # rubocop:disable Lint/UnreachableCode + assert_equal @expected_meta, association.meta end # rubocop:enable Metrics/AbcSize @@ -319,7 +333,9 @@ module ActiveModel assert_nil reflection.options.fetch(:meta) # Assert before instance_eval link - association.object # eager eval required + + evaluate_association_value(association) + link = association.links.fetch(:self) assert_nil reflection.options.fetch(:meta) assert_respond_to link, :call @@ -330,12 +346,11 @@ module ActiveModel assert_respond_to association.links.fetch(:self), :call # Assert before instance_eval link meta assert_respond_to reflection.options.fetch(:meta), :call - return # oh no, need to figure this out - assert_nil association.meta # rubocop:disable Lint/UnreachableCode + assert_respond_to association.meta, :call # Assert after instance_eval link meta assert_equal @expected_meta, reflection.instance_eval(&reflection.options.fetch(:meta)) - assert_nil association.meta + assert_respond_to association.meta, :call end # rubocop:enable Metrics/AbcSize @@ -356,7 +371,9 @@ module ActiveModel # Build Association association = reflection.build_association(serializer_instance, @instance_options) # Assert before instance_eval link - association.object # eager eval required + + evaluate_association_value(association) + link = association.links.fetch(:self) assert_respond_to link, :call @@ -380,7 +397,9 @@ module ActiveModel reflection = serializer_class._reflections.fetch(:blog) assert_nil reflection.options.fetch(:meta) association = reflection.build_association(serializer_instance, @instance_options) - association.object # eager eval required + + evaluate_association_value(association) + assert_equal model1_meta, association.meta assert_equal model1_meta, reflection.options.fetch(:meta) @@ -396,7 +415,9 @@ module ActiveModel assert_equal model1_meta, reflection.options.fetch(:meta) association = reflection.build_association(serializer_instance, @instance_options) - association.object # eager eval required + + evaluate_association_value(association) + assert_equal model2_meta, association.meta assert_equal model2_meta, reflection.options.fetch(:meta) end From 320596b75bf616d06657a62492ee3cb1a6f80b9b Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Fri, 31 Mar 2017 10:30:57 -0500 Subject: [PATCH 39/47] Undef problematic Object methods --- lib/active_model/serializer.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index b50cb951..9d00e6fb 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -12,6 +12,9 @@ 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 From 4fb635bd2925882eb9116e1e210657bb7f2912dd Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Fri, 31 Mar 2017 10:42:25 -0500 Subject: [PATCH 40/47] Required --- lib/active_model_serializers/model.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/active_model_serializers/model.rb b/lib/active_model_serializers/model.rb index ea10ac88..2ff3d60c 100644 --- a/lib/active_model_serializers/model.rb +++ b/lib/active_model_serializers/model.rb @@ -1,6 +1,7 @@ # 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 From 273b7e7f3031a5867e473a4a1e079351ae90fcbf Mon Sep 17 00:00:00 2001 From: Manuel Thomassen Date: Wed, 25 May 2016 09:27:37 +0200 Subject: [PATCH 41/47] belongs_to causes unnecessary db hit --- test/serializers/associations_test.rb | 28 +++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/test/serializers/associations_test.rb b/test/serializers/associations_test.rb index f6603a8d..c1b164b8 100644 --- a/test/serializers/associations_test.rb +++ b/test/serializers/associations_test.rb @@ -137,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 From 6e4152851554b33d8cdf607fe4e175dd1aafd1c7 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 30 Apr 2017 18:31:35 -0500 Subject: [PATCH 42/47] Skip eval relationships object on belongs to --- lib/active_model/serializer/association.rb | 4 ++++ .../serializer/belongs_to_reflection.rb | 4 ++++ lib/active_model/serializer/reflection.rb | 17 +++++++++++++++ .../adapter/json_api/relationship.rb | 21 ++++++++++++------- .../adapter/json_api/resource_identifier.rb | 7 +++++++ 5 files changed, 46 insertions(+), 7 deletions(-) diff --git a/lib/active_model/serializer/association.rb b/lib/active_model/serializer/association.rb index 767fd8a2..7ce82316 100644 --- a/lib/active_model/serializer/association.rb +++ b/lib/active_model/serializer/association.rb @@ -38,6 +38,10 @@ module ActiveModel reflection.options[:meta] end + def belongs_to? + reflection.foreign_key_on == :self + end + def polymorphic? true == reflection_options[:polymorphic] end diff --git a/lib/active_model/serializer/belongs_to_reflection.rb b/lib/active_model/serializer/belongs_to_reflection.rb index 67dbe79a..04bbc6fc 100644 --- a/lib/active_model/serializer/belongs_to_reflection.rb +++ b/lib/active_model/serializer/belongs_to_reflection.rb @@ -2,6 +2,10 @@ module ActiveModel class Serializer # @api private class BelongsToReflection < Reflection + # @api private + def foreign_key_on + :self + end end end end diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index 49eb7600..d3daab4b 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -47,11 +47,23 @@ module ActiveModel # # So you can inspect reflections in your Adapters. class Reflection < Field + attr_reader :foreign_key, :type + def initialize(*) super 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 = + if collection? + "#{name.to_s.singularize}_ids".to_sym + else + "#{name}_id".to_sym + end end # @api public @@ -150,6 +162,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. # diff --git a/lib/active_model_serializers/adapter/json_api/relationship.rb b/lib/active_model_serializers/adapter/json_api/relationship.rb index fc2f116f..5d7399a3 100644 --- a/lib/active_model_serializers/adapter/json_api/relationship.rb +++ b/lib/active_model_serializers/adapter/json_api/relationship.rb @@ -43,14 +43,21 @@ module ActiveModelSerializers end def data_for_one(association) - # 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 + 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 - nil + # 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 diff --git a/lib/active_model_serializers/adapter/json_api/resource_identifier.rb b/lib/active_model_serializers/adapter/json_api/resource_identifier.rb index af6f5f9e..8931f899 100644 --- a/lib/active_model_serializers/adapter/json_api/resource_identifier.rb +++ b/lib/active_model_serializers/adapter/json_api/resource_identifier.rb @@ -22,6 +22,13 @@ module ActiveModelSerializers JsonApi.send(:transform_key_casing!, raw_type, transform_options) end + def self.for_type_with_id(type, id, options) + { + 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) From c9b0e4e6aeb79b7c4f0a8d80c069df95258fc54c Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 30 Apr 2017 23:00:40 -0500 Subject: [PATCH 43/47] Do not calculate cache_key unless caching --- lib/active_model/serializer/concerns/caching.rb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/active_model/serializer/concerns/caching.rb b/lib/active_model/serializer/concerns/caching.rb index 1de86246..2a030b68 100644 --- a/lib/active_model/serializer/concerns/caching.rb +++ b/lib/active_model/serializer/concerns/caching.rb @@ -226,8 +226,9 @@ module ActiveModel end end - def fetch(adapter_instance, cache_options = serializer_class._cache_options, key = cache_key(adapter_instance)) + def fetch(adapter_instance, cache_options = serializer_class._cache_options, key = nil) if serializer_class.cache_store + key ||= cache_key(adapter_instance) serializer_class.cache_store.fetch(key, cache_options) do yield end From 73eae19b3d0b8a3c03fda566a208eeee6720beed Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Sun, 30 Apr 2017 22:52:41 -0500 Subject: [PATCH 44/47] Return null resource object identifier for blank id Also, fix test where attributes were included when id was "" ``` 1) Failure: ActionController::Serialization::AdapterSelectorTest#test_render_using_adapter_override [test/action_c$ntroller/adapter_selector_test.rb:53]: --- expected +++ actual @@ -1 +1 @@ -"{\"data\":{\"id\":\"\",\"type\":\"profiles\",\"attributes\":{\"name\":\"Name 1\",\"description\":\"Description 1\"}}}" +"{\"data\":null}" ``` --- .../adapter/json_api.rb | 44 ++++++++++++------- .../adapter/json_api/resource_identifier.rb | 2 + .../adapter_selector_test.rb | 4 +- 3 files changed, 32 insertions(+), 18 deletions(-) diff --git a/lib/active_model_serializers/adapter/json_api.rb b/lib/active_model_serializers/adapter/json_api.rb index c252deaf..b225416b 100644 --- a/lib/active_model_serializers/adapter/json_api.rb +++ b/lib/active_model_serializers/adapter/json_api.rb @@ -295,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 @@ -322,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 @@ -332,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: diff --git a/lib/active_model_serializers/adapter/json_api/resource_identifier.rb b/lib/active_model_serializers/adapter/json_api/resource_identifier.rb index 8931f899..3a235f2b 100644 --- a/lib/active_model_serializers/adapter/json_api/resource_identifier.rb +++ b/lib/active_model_serializers/adapter/json_api/resource_identifier.rb @@ -23,6 +23,7 @@ module ActiveModelSerializers 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) @@ -36,6 +37,7 @@ module ActiveModelSerializers end def as_json + return nil if id.blank? { id: id, type: type } end diff --git a/test/action_controller/adapter_selector_test.rb b/test/action_controller/adapter_selector_test.rb index db93573b..3373de7c 100644 --- a/test/action_controller/adapter_selector_test.rb +++ b/test/action_controller/adapter_selector_test.rb @@ -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', From 96028a7b9978adb9aa839aa332c35e0c244896aa Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Mon, 1 May 2017 10:17:48 -0500 Subject: [PATCH 45/47] Document new reflection options; support :foreign_key --- docs/general/serializers.md | 3 +++ lib/active_model/serializer/reflection.rb | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/general/serializers.md b/docs/general/serializers.md index 1d968183..0606c51d 100644 --- a/docs/general/serializers.md +++ b/docs/general/serializers.md @@ -64,6 +64,9 @@ 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. - 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. diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index d3daab4b..2e5cc2a1 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -9,6 +9,7 @@ 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) @@ -58,12 +59,13 @@ module ActiveModel class_name = options.fetch(:class_name, name.to_s.camelize.singularize) class_name.underscore.pluralize.to_sym end - @foreign_key = + @foreign_key = options.fetch(:foreign_key) do if collection? "#{name.to_s.singularize}_ids".to_sym else "#{name}_id".to_sym end + end end # @api public From ec7b5859f77882892325840eae0716fcaab6c14f Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Mon, 1 May 2017 10:23:13 -0500 Subject: [PATCH 46/47] Document namespace --- docs/general/serializers.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/general/serializers.md b/docs/general/serializers.md index 0606c51d..5b23ba0f 100644 --- a/docs/general/serializers.md +++ b/docs/general/serializers.md @@ -67,6 +67,7 @@ Where: - `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. From dff621e1744d37672ccba13f8e4b64dcc5b135c3 Mon Sep 17 00:00:00 2001 From: Benjamin Fleischer Date: Mon, 1 May 2017 10:10:54 -0500 Subject: [PATCH 47/47] Bump to v0.10.6 --- CHANGELOG.md | 13 ++++++++++++- README.md | 4 ++-- lib/active_model/serializer/version.rb | 2 +- 3 files changed, 15 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fc263078..c975e473 100644 --- a/CHANGELOG.md +++ b/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: @@ -10,8 +10,19 @@ 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) diff --git a/README.md b/README.md index d069dcf5..5bdcd20d 100644 --- a/README.md +++ b/README.md @@ -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.5 (latest release) Documentation](https://github.com/rails-api/active_model_serializers/tree/v0.10.5) - - [![API Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/gems/active_model_serializers/0.10.5) +- [0.10.6 (latest release) Documentation](https://github.com/rails-api/active_model_serializers/tree/v0.10.6) + - [![API Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/gems/active_model_serializers/0.10.6) - [Guides](docs) - [0.9 (0-9-stable) Documentation](https://github.com/rails-api/active_model_serializers/tree/0-9-stable) - [![API Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/github/rails-api/active_model_serializers/0-9-stable) diff --git a/lib/active_model/serializer/version.rb b/lib/active_model/serializer/version.rb index 209437da..e692240a 100644 --- a/lib/active_model/serializer/version.rb +++ b/lib/active_model/serializer/version.rb @@ -1,5 +1,5 @@ module ActiveModel class Serializer - VERSION = '0.10.5'.freeze + VERSION = '0.10.6'.freeze end end