mirror of
https://github.com/ditkrg/active_model_serializers.git
synced 2026-01-25 15:23:06 +00:00
Clean slate
This commit is contained in:
@@ -1,66 +0,0 @@
|
||||
require 'active_support/core_ext/class/attribute'
|
||||
require 'active_model_serializers/serialization_context'
|
||||
|
||||
module ActionController
|
||||
module Serialization
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
include ActionController::Renderers
|
||||
|
||||
module ClassMethods
|
||||
def serialization_scope(scope)
|
||||
self._serialization_scope = scope
|
||||
end
|
||||
end
|
||||
|
||||
included do
|
||||
class_attribute :_serialization_scope
|
||||
self._serialization_scope = :current_user
|
||||
|
||||
attr_writer :namespace_for_serializer
|
||||
end
|
||||
|
||||
def namespace_for_serializer
|
||||
@namespace_for_serializer ||= self.class.parent unless self.class.parent == Object
|
||||
end
|
||||
|
||||
def serialization_scope
|
||||
return unless _serialization_scope && respond_to?(_serialization_scope, true)
|
||||
|
||||
send(_serialization_scope)
|
||||
end
|
||||
|
||||
def get_serializer(resource, options = {})
|
||||
unless use_adapter?
|
||||
warn 'ActionController::Serialization#use_adapter? has been removed. '\
|
||||
"Please pass 'adapter: false' or see ActiveSupport::SerializableResource.new"
|
||||
options[:adapter] = false
|
||||
end
|
||||
|
||||
options.fetch(:namespace) { options[:namespace] = namespace_for_serializer }
|
||||
|
||||
serializable_resource = ActiveModelSerializers::SerializableResource.new(resource, options)
|
||||
serializable_resource.serialization_scope ||= options.fetch(:scope) { serialization_scope }
|
||||
serializable_resource.serialization_scope_name = options.fetch(:scope_name) { _serialization_scope }
|
||||
# For compatibility with the JSON renderer: `json.to_json(options) if json.is_a?(String)`.
|
||||
# Otherwise, since `serializable_resource` is not a string, the renderer would call
|
||||
# `to_json` on a String and given odd results, such as `"".to_json #=> '""'`
|
||||
serializable_resource.adapter.is_a?(String) ? serializable_resource.adapter : serializable_resource
|
||||
end
|
||||
|
||||
# Deprecated
|
||||
def use_adapter?
|
||||
true
|
||||
end
|
||||
|
||||
[:_render_option_json, :_render_with_renderer_json].each do |renderer_method|
|
||||
define_method renderer_method do |resource, options|
|
||||
options.fetch(:serialization_context) do
|
||||
options[:serialization_context] = ActiveModelSerializers::SerializationContext.new(request, options)
|
||||
end
|
||||
serializable_resource = get_serializer(resource, options)
|
||||
super(serializable_resource, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,11 +0,0 @@
|
||||
require 'set'
|
||||
|
||||
module ActiveModel
|
||||
class SerializableResource
|
||||
class << self
|
||||
extend ActiveModelSerializers::Deprecate
|
||||
|
||||
delegate_and_deprecate :new, ActiveModelSerializers::SerializableResource
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,448 +0,0 @@
|
||||
require 'thread_safe'
|
||||
require 'jsonapi/include_directive'
|
||||
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/caching'
|
||||
require 'active_model/serializer/fieldset'
|
||||
require 'active_model/serializer/lint'
|
||||
|
||||
# ActiveModel::Serializer is an abstract class that is
|
||||
# reified when subclassed to decorate a resource.
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
# @see #serializable_hash for more details on these valid keys.
|
||||
SERIALIZABLE_HASH_VALID_KEYS = [:only, :except, :methods, :include, :root].freeze
|
||||
extend ActiveSupport::Autoload
|
||||
autoload :Adapter
|
||||
autoload :Null
|
||||
autoload :Attribute
|
||||
autoload :Association
|
||||
autoload :Reflection
|
||||
autoload :SingularReflection
|
||||
autoload :CollectionReflection
|
||||
autoload :BelongsToReflection
|
||||
autoload :HasOneReflection
|
||||
autoload :HasManyReflection
|
||||
include ActiveSupport::Configurable
|
||||
include Caching
|
||||
|
||||
# @param resource [ActiveRecord::Base, ActiveModelSerializers::Model]
|
||||
# @return [ActiveModel::Serializer]
|
||||
# Preferentially returns
|
||||
# 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_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
|
||||
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
|
||||
|
||||
# @see ActiveModelSerializers::Adapter.lookup
|
||||
# Deprecated
|
||||
def self.adapter
|
||||
ActiveModelSerializers::Adapter.lookup(config.adapter)
|
||||
end
|
||||
class << self
|
||||
extend ActiveModelSerializers::Deprecate
|
||||
deprecate :adapter, 'ActiveModelSerializers::Adapter.configured_adapter'
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.serializer_lookup_chain_for(klass, namespace = nil)
|
||||
lookups = ActiveModelSerializers.config.serializer_lookup_chain
|
||||
Array[*lookups].flat_map do |lookup|
|
||||
lookup.call(klass, self, namespace)
|
||||
end.compact
|
||||
end
|
||||
|
||||
# Used to cache serializer name => serializer class
|
||||
# when looked up by Serializer.get_serializer_for.
|
||||
def self.serializers_cache
|
||||
@serializers_cache ||= ThreadSafe::Cache.new
|
||||
end
|
||||
|
||||
# @api private
|
||||
# Find a serializer from a class and caches the lookup.
|
||||
# Preferentially returns:
|
||||
# 1. class name appended with "Serializer"
|
||||
# 2. try again with superclass, if present
|
||||
# 3. nil
|
||||
def self.get_serializer_for(klass, namespace = nil)
|
||||
return nil unless config.serializer_lookup_enabled
|
||||
|
||||
cache_key = ActiveSupport::Cache.expand_cache_key(klass, namespace)
|
||||
serializers_cache.fetch_or_store(cache_key) do
|
||||
# NOTE(beauby): When we drop 1.9.3 support we can lazify the map for perfs.
|
||||
lookup_chain = serializer_lookup_chain_for(klass, namespace)
|
||||
serializer_class = lookup_chain.map(&:safe_constantize).find { |x| x && x < ActiveModel::Serializer }
|
||||
|
||||
if serializer_class
|
||||
serializer_class
|
||||
elsif klass.superclass
|
||||
get_serializer_for(klass.superclass)
|
||||
else
|
||||
nil # No serializer found
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.include_directive_from_options(options)
|
||||
if options[:include_directive]
|
||||
options[:include_directive]
|
||||
elsif options[:include]
|
||||
JSONAPI::IncludeDirective.new(options[:include], allow_wildcard: true)
|
||||
else
|
||||
ActiveModelSerializers.default_include_directive
|
||||
end
|
||||
end
|
||||
|
||||
# @api private
|
||||
def self.serialization_adapter_instance
|
||||
@serialization_adapter_instance ||= ActiveModelSerializers::Adapter::Attributes
|
||||
end
|
||||
|
||||
# Preferred interface is ActiveModelSerializers.config
|
||||
# BEGIN DEFAULT CONFIGURATION
|
||||
config.collection_serializer = ActiveModel::Serializer::CollectionSerializer
|
||||
config.serializer_lookup_enabled = true
|
||||
|
||||
# @deprecated Use {#config.collection_serializer=} instead of this. Is
|
||||
# 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'
|
||||
# END DEFAULT CONFIGURATION
|
||||
|
||||
with_options instance_writer: false, instance_reader: false do |serializer|
|
||||
serializer.class_attribute :_attributes_data # @api private
|
||||
self._attributes_data ||= {}
|
||||
end
|
||||
with_options instance_writer: false, instance_reader: true do |serializer|
|
||||
serializer.class_attribute :_reflections
|
||||
self._reflections ||= {}
|
||||
serializer.class_attribute :_links # @api private
|
||||
self._links ||= {}
|
||||
serializer.class_attribute :_meta # @api private
|
||||
serializer.class_attribute :_type # @api private
|
||||
end
|
||||
|
||||
def self.inherited(base)
|
||||
super
|
||||
base._attributes_data = _attributes_data.dup
|
||||
base._reflections = _reflections.dup
|
||||
base._links = _links.dup
|
||||
end
|
||||
|
||||
# @return [Array<Symbol>] Key names of declared attributes
|
||||
# @see Serializer::attribute
|
||||
def self._attributes
|
||||
_attributes_data.keys
|
||||
end
|
||||
|
||||
# BEGIN SERIALIZER MACROS
|
||||
|
||||
# @example
|
||||
# class AdminAuthorSerializer < ActiveModel::Serializer
|
||||
# attributes :id, :name, :recent_edits
|
||||
def self.attributes(*attrs)
|
||||
attrs = attrs.first if attrs.first.class == Array
|
||||
|
||||
attrs.each do |attr|
|
||||
attribute(attr)
|
||||
end
|
||||
end
|
||||
|
||||
# @example
|
||||
# class AdminAuthorSerializer < ActiveModel::Serializer
|
||||
# attributes :id, :recent_edits
|
||||
# attribute :name, key: :title
|
||||
#
|
||||
# attribute :full_name do
|
||||
# "#{object.first_name} #{object.last_name}"
|
||||
# end
|
||||
#
|
||||
# def recent_edits
|
||||
# object.edits.last(5)
|
||||
# end
|
||||
def self.attribute(attr, options = {}, &block)
|
||||
key = options.fetch(:key, attr)
|
||||
_attributes_data[key] = Attribute.new(attr, options, block)
|
||||
end
|
||||
|
||||
# @param [Symbol] name of the association
|
||||
# @param [Hash<Symbol => any>] options for the reflection
|
||||
# @return [void]
|
||||
#
|
||||
# @example
|
||||
# has_many :comments, serializer: CommentSummarySerializer
|
||||
#
|
||||
def self.has_many(name, options = {}, &block) # rubocop:disable Style/PredicateName
|
||||
associate(HasManyReflection.new(name, options, block))
|
||||
end
|
||||
|
||||
# @param [Symbol] name of the association
|
||||
# @param [Hash<Symbol => any>] options for the reflection
|
||||
# @return [void]
|
||||
#
|
||||
# @example
|
||||
# belongs_to :author, serializer: AuthorSerializer
|
||||
#
|
||||
def self.belongs_to(name, options = {}, &block)
|
||||
associate(BelongsToReflection.new(name, options, block))
|
||||
end
|
||||
|
||||
# @param [Symbol] name of the association
|
||||
# @param [Hash<Symbol => any>] options for the reflection
|
||||
# @return [void]
|
||||
#
|
||||
# @example
|
||||
# has_one :author, serializer: AuthorSerializer
|
||||
#
|
||||
def self.has_one(name, options = {}, &block) # rubocop:disable Style/PredicateName
|
||||
associate(HasOneReflection.new(name, options, block))
|
||||
end
|
||||
|
||||
# Add reflection and define {name} accessor.
|
||||
# @param [ActiveModel::Serializer::Reflection] reflection
|
||||
# @return [void]
|
||||
#
|
||||
# @api private
|
||||
def self.associate(reflection)
|
||||
key = reflection.options[:key] || reflection.name
|
||||
self._reflections[key] = reflection
|
||||
end
|
||||
private_class_method :associate
|
||||
|
||||
# Define a link on a serializer.
|
||||
# @example
|
||||
# link(:self) { resource_url(object) }
|
||||
# @example
|
||||
# link(:self) { "http://example.com/resource/#{object.id}" }
|
||||
# @example
|
||||
# link :resource, "http://example.com/resource"
|
||||
#
|
||||
def self.link(name, value = nil, &block)
|
||||
_links[name] = block || value
|
||||
end
|
||||
|
||||
# Set the JSON API meta attribute of a serializer.
|
||||
# @example
|
||||
# class AdminAuthorSerializer < ActiveModel::Serializer
|
||||
# meta { stuff: 'value' }
|
||||
# @example
|
||||
# meta do
|
||||
# { comment_count: object.comments.count }
|
||||
# end
|
||||
def self.meta(value = nil, &block)
|
||||
self._meta = block || value
|
||||
end
|
||||
|
||||
# Set the JSON API type of a serializer.
|
||||
# @example
|
||||
# class AdminAuthorSerializer < ActiveModel::Serializer
|
||||
# type 'authors'
|
||||
def self.type(type)
|
||||
self._type = type && type.to_s
|
||||
end
|
||||
|
||||
# END SERIALIZER MACROS
|
||||
|
||||
attr_accessor :object, :root, :scope
|
||||
|
||||
# `scope_name` is set as :current_user by default in the controller.
|
||||
# If the instance does not have a method named `scope_name`, it
|
||||
# defines the method so that it calls the +scope+.
|
||||
def initialize(object, options = {})
|
||||
self.object = object
|
||||
self.instance_options = options
|
||||
self.root = instance_options[:root]
|
||||
self.scope = instance_options[:scope]
|
||||
|
||||
return if !(scope_name = instance_options[:scope_name]) || respond_to?(scope_name)
|
||||
|
||||
define_singleton_method scope_name, -> { scope }
|
||||
end
|
||||
|
||||
def success?
|
||||
true
|
||||
end
|
||||
|
||||
# Return the +attributes+ of +object+ as presented
|
||||
# by the serializer.
|
||||
def attributes(requested_attrs = nil, reload = false)
|
||||
@attributes = nil if reload
|
||||
@attributes ||= self.class._attributes_data.each_with_object({}) do |(key, attr), hash|
|
||||
next if attr.excluded?(self)
|
||||
next unless requested_attrs.nil? || requested_attrs.include?(key)
|
||||
hash[key] = attr.value(self)
|
||||
end
|
||||
end
|
||||
|
||||
# @param [JSONAPI::IncludeDirective] include_directive (defaults to the
|
||||
# +default_include_directive+ config value when not provided)
|
||||
# @return [Enumerator<Association>]
|
||||
#
|
||||
def associations(include_directive = ActiveModelSerializers.default_include_directive, include_slice = nil)
|
||||
include_slice ||= include_directive
|
||||
return unless object
|
||||
|
||||
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.
|
||||
#
|
||||
# TODO: Include <tt>ActiveModel::Serializers::JSON</tt>.
|
||||
# So that the below is true:
|
||||
# @param options [nil, Hash] The same valid options passed to `serializable_hash`
|
||||
# (:only, :except, :methods, and :include).
|
||||
#
|
||||
# See
|
||||
# https://github.com/rails/rails/blob/v5.0.0.beta2/activemodel/lib/active_model/serializers/json.rb#L17-L101
|
||||
# https://github.com/rails/rails/blob/v5.0.0.beta2/activemodel/lib/active_model/serialization.rb#L85-L123
|
||||
# https://github.com/rails/rails/blob/v5.0.0.beta2/activerecord/lib/active_record/serialization.rb#L11-L17
|
||||
# https://github.com/rails/rails/blob/v5.0.0.beta2/activesupport/lib/active_support/core_ext/object/json.rb#L147-L162
|
||||
#
|
||||
# @example
|
||||
# # The :only and :except options can be used to limit the attributes included, and work
|
||||
# # similar to the attributes method.
|
||||
# serializer.as_json(only: [:id, :name])
|
||||
# serializer.as_json(except: [:id, :created_at, :age])
|
||||
#
|
||||
# # To include the result of some method calls on the model use :methods:
|
||||
# serializer.as_json(methods: :permalink)
|
||||
#
|
||||
# # To include associations use :include:
|
||||
# serializer.as_json(include: :posts)
|
||||
# # Second level and higher order associations work as well:
|
||||
# serializer.as_json(include: { posts: { include: { comments: { only: :body } }, only: :title } })
|
||||
def serializable_hash(adapter_options = nil, options = {}, adapter_instance = self.class.serialization_adapter_instance)
|
||||
adapter_options ||= {}
|
||||
options[:include_directive] ||= ActiveModel::Serializer.include_directive_from_options(adapter_options)
|
||||
cached_attributes = adapter_options[:cached_attributes] ||= {}
|
||||
resource = fetch_attributes(options[:fields], cached_attributes, adapter_instance)
|
||||
relationships = resource_relationships(adapter_options, options, adapter_instance)
|
||||
resource.merge(relationships)
|
||||
end
|
||||
alias to_hash serializable_hash
|
||||
alias to_h serializable_hash
|
||||
|
||||
# @see #serializable_hash
|
||||
# TODO: When moving attributes adapter logic here, @see #serializable_hash
|
||||
# So that the below is true:
|
||||
# @param options [nil, Hash] The same valid options passed to `as_json`
|
||||
# (:root, :only, :except, :methods, and :include).
|
||||
# The default for `root` is nil.
|
||||
# The default value for include_root is false. You can change it to true if the given
|
||||
# JSON string includes a single root node.
|
||||
def as_json(adapter_opts = nil)
|
||||
serializable_hash(adapter_opts)
|
||||
end
|
||||
|
||||
# Used by adapter as resource root.
|
||||
def json_key
|
||||
root || _type || object.class.model_name.to_s.underscore
|
||||
end
|
||||
|
||||
def read_attribute_for_serialization(attr)
|
||||
if respond_to?(attr)
|
||||
send(attr)
|
||||
else
|
||||
object.read_attribute_for_serialization(attr)
|
||||
end
|
||||
end
|
||||
|
||||
# @api private
|
||||
def resource_relationships(adapter_options, options, adapter_instance)
|
||||
relationships = {}
|
||||
include_directive = options.fetch(:include_directive)
|
||||
associations(include_directive).each do |association|
|
||||
adapter_opts = adapter_options.merge(include_directive: include_directive[association.key])
|
||||
relationships[association.key] ||= relationship_value_for(association, adapter_opts, adapter_instance)
|
||||
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
|
||||
end
|
||||
end
|
||||
@@ -1,24 +0,0 @@
|
||||
require 'active_model_serializers/adapter'
|
||||
require 'active_model_serializers/deprecate'
|
||||
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
# @deprecated Use ActiveModelSerializers::Adapter instead
|
||||
module Adapter
|
||||
class << self
|
||||
extend ActiveModelSerializers::Deprecate
|
||||
|
||||
DEPRECATED_METHODS = [:create, :adapter_class, :adapter_map, :adapters, :register, :lookup].freeze
|
||||
DEPRECATED_METHODS.each do |method|
|
||||
delegate_and_deprecate method, ActiveModelSerializers::Adapter
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
require 'active_model/serializer/adapter/base'
|
||||
require 'active_model/serializer/adapter/null'
|
||||
require 'active_model/serializer/adapter/attributes'
|
||||
require 'active_model/serializer/adapter/json'
|
||||
require 'active_model/serializer/adapter/json_api'
|
||||
@@ -1,15 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
module Adapter
|
||||
class Attributes < DelegateClass(ActiveModelSerializers::Adapter::Attributes)
|
||||
def initialize(serializer, options = {})
|
||||
super(ActiveModelSerializers::Adapter::Attributes.new(serializer, options))
|
||||
end
|
||||
class << self
|
||||
extend ActiveModelSerializers::Deprecate
|
||||
deprecate :new, 'ActiveModelSerializers::Adapter::Json.'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,18 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
module Adapter
|
||||
class Base < DelegateClass(ActiveModelSerializers::Adapter::Base)
|
||||
class << self
|
||||
extend ActiveModelSerializers::Deprecate
|
||||
deprecate :inherited, 'ActiveModelSerializers::Adapter::Base.'
|
||||
end
|
||||
|
||||
# :nocov:
|
||||
def initialize(serializer, options = {})
|
||||
super(ActiveModelSerializers::Adapter::Base.new(serializer, options))
|
||||
end
|
||||
# :nocov:
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,15 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
module Adapter
|
||||
class Json < DelegateClass(ActiveModelSerializers::Adapter::Json)
|
||||
def initialize(serializer, options = {})
|
||||
super(ActiveModelSerializers::Adapter::Json.new(serializer, options))
|
||||
end
|
||||
class << self
|
||||
extend ActiveModelSerializers::Deprecate
|
||||
deprecate :new, 'ActiveModelSerializers::Adapter::Json.new'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,15 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
module Adapter
|
||||
class JsonApi < DelegateClass(ActiveModelSerializers::Adapter::JsonApi)
|
||||
def initialize(serializer, options = {})
|
||||
super(ActiveModelSerializers::Adapter::JsonApi.new(serializer, options))
|
||||
end
|
||||
class << self
|
||||
extend ActiveModelSerializers::Deprecate
|
||||
deprecate :new, 'ActiveModelSerializers::Adapter::JsonApi.new'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,15 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
module Adapter
|
||||
class Null < DelegateClass(ActiveModelSerializers::Adapter::Null)
|
||||
def initialize(serializer, options = {})
|
||||
super(ActiveModelSerializers::Adapter::Null.new(serializer, options))
|
||||
end
|
||||
class << self
|
||||
extend ActiveModelSerializers::Deprecate
|
||||
deprecate :new, 'ActiveModelSerializers::Adapter::Null.new'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,12 +0,0 @@
|
||||
require 'active_model/serializer/collection_serializer'
|
||||
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
class ArraySerializer < CollectionSerializer
|
||||
class << self
|
||||
extend ActiveModelSerializers::Deprecate
|
||||
deprecate :new, 'ActiveModel::Serializer::CollectionSerializer.'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,34 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
# This class holds all information about serializer's association.
|
||||
#
|
||||
# @attr [Symbol] name
|
||||
# @attr [Hash{Symbol => Object}] options
|
||||
# @attr [block]
|
||||
#
|
||||
# @example
|
||||
# Association.new(:comments, { serializer: CommentSummarySerializer })
|
||||
#
|
||||
class Association < Field
|
||||
# @return [Symbol]
|
||||
def key
|
||||
options.fetch(:key, name)
|
||||
end
|
||||
|
||||
# @return [ActiveModel::Serializer, nil]
|
||||
def serializer
|
||||
options[:serializer]
|
||||
end
|
||||
|
||||
# @return [Hash]
|
||||
def links
|
||||
options.fetch(:links) || {}
|
||||
end
|
||||
|
||||
# @return [Hash, nil]
|
||||
def meta
|
||||
options[:meta]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,25 +0,0 @@
|
||||
require 'active_model/serializer/field'
|
||||
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
# Holds all the meta-data about an attribute as it was specified in the
|
||||
# ActiveModel::Serializer class.
|
||||
#
|
||||
# @example
|
||||
# class PostSerializer < ActiveModel::Serializer
|
||||
# attribute :content
|
||||
# attribute :name, key: :title
|
||||
# attribute :email, key: :author_email, if: :user_logged_in?
|
||||
# attribute :preview do
|
||||
# truncate(object.content)
|
||||
# end
|
||||
#
|
||||
# def user_logged_in?
|
||||
# current_user.logged_in?
|
||||
# end
|
||||
# end
|
||||
#
|
||||
class Attribute < Field
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
# @api private
|
||||
class BelongsToReflection < SingularReflection
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
# @api private
|
||||
class CollectionReflection < Reflection
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,87 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
class CollectionSerializer
|
||||
include Enumerable
|
||||
delegate :each, to: :@serializers
|
||||
|
||||
attr_reader :object, :root
|
||||
|
||||
def initialize(resources, options = {})
|
||||
@object = resources
|
||||
@options = options
|
||||
@root = options[:root]
|
||||
@serializers = serializers_from_resources
|
||||
end
|
||||
|
||||
def success?
|
||||
true
|
||||
end
|
||||
|
||||
# @api private
|
||||
def serializable_hash(adapter_options, options, adapter_instance)
|
||||
include_directive = ActiveModel::Serializer.include_directive_from_options(adapter_options)
|
||||
adapter_options[:cached_attributes] ||= ActiveModel::Serializer.cache_read_multi(self, adapter_instance, include_directive)
|
||||
adapter_opts = adapter_options.merge(include_directive: include_directive)
|
||||
serializers.map do |serializer|
|
||||
serializer.serializable_hash(adapter_opts, options, adapter_instance)
|
||||
end
|
||||
end
|
||||
|
||||
# TODO: unify naming of root, json_key, and _type. Right now, a serializer's
|
||||
# json_key comes from the root option or the object's model name, by default.
|
||||
# But, if a dev defines a custom `json_key` method with an explicit value,
|
||||
# we have no simple way to know that it is safe to call that instance method.
|
||||
# (which is really a class property at this point, anyhow).
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
# Disabling cop since it's good to highlight the complexity of this method by
|
||||
# including all the logic right here.
|
||||
def json_key
|
||||
return root if root
|
||||
# 1. get from options[:serializer] for empty resource collection
|
||||
key = object.empty? &&
|
||||
(explicit_serializer_class = options[:serializer]) &&
|
||||
explicit_serializer_class._type
|
||||
# 2. get from first serializer instance in collection
|
||||
key ||= (serializer = serializers.first) && serializer.json_key
|
||||
# 3. get from collection name, if a named collection
|
||||
key ||= object.respond_to?(:name) ? object.name && object.name.underscore : nil
|
||||
# 4. key may be nil for empty collection and no serializer option
|
||||
key && key.pluralize
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
|
||||
def paginated?
|
||||
ActiveModelSerializers.config.jsonapi_pagination_links_enabled &&
|
||||
object.respond_to?(:current_page) &&
|
||||
object.respond_to?(:total_pages) &&
|
||||
object.respond_to?(:size)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :serializers, :options
|
||||
|
||||
private
|
||||
|
||||
def serializers_from_resources
|
||||
serializer_context_class = options.fetch(:serializer_context_class, ActiveModel::Serializer)
|
||||
object.map do |resource|
|
||||
serializer_from_resource(resource, serializer_context_class, options)
|
||||
end
|
||||
end
|
||||
|
||||
def serializer_from_resource(resource, serializer_context_class, options)
|
||||
serializer_class = options.fetch(:serializer) do
|
||||
serializer_context_class.serializer_for(resource, namespace: options[:namespace])
|
||||
end
|
||||
|
||||
if serializer_class.nil?
|
||||
ActiveModelSerializers.logger.debug "No serializer found for resource: #{resource.inspect}"
|
||||
throw :no_serializer
|
||||
else
|
||||
serializer_class.new(resource, options.except(:serializer))
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,304 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
UndefinedCacheKey = Class.new(StandardError)
|
||||
module Caching
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
with_options instance_writer: false, instance_reader: false do |serializer|
|
||||
serializer.class_attribute :_cache # @api private : the cache store
|
||||
serializer.class_attribute :_cache_key # @api private : when present, is first item in cache_key. Ignored if the serializable object defines #cache_key.
|
||||
serializer.class_attribute :_cache_only # @api private : when fragment caching, whitelists fetch_attributes. Cannot combine with except
|
||||
serializer.class_attribute :_cache_except # @api private : when fragment caching, blacklists fetch_attributes. Cannot combine with only
|
||||
serializer.class_attribute :_cache_options # @api private : used by CachedSerializer, passed to _cache.fetch
|
||||
# _cache_options include:
|
||||
# expires_in
|
||||
# compress
|
||||
# force
|
||||
# race_condition_ttl
|
||||
# Passed to ::_cache as
|
||||
# serializer.cache_store.fetch(cache_key, @klass._cache_options)
|
||||
# Passed as second argument to serializer.cache_store.fetch(cache_key, serializer_class._cache_options)
|
||||
serializer.class_attribute :_cache_digest_file_path # @api private : Derived at inheritance
|
||||
end
|
||||
end
|
||||
|
||||
# Matches
|
||||
# "c:/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb:1:in `<top (required)>'"
|
||||
# AND
|
||||
# "/c/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb:1:in `<top (required)>'"
|
||||
# AS
|
||||
# c/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb
|
||||
CALLER_FILE = /
|
||||
\A # start of string
|
||||
.+ # file path (one or more characters)
|
||||
(?= # stop previous match when
|
||||
:\d+ # a colon is followed by one or more digits
|
||||
:in # followed by a colon followed by in
|
||||
)
|
||||
/x
|
||||
|
||||
module ClassMethods
|
||||
def inherited(base)
|
||||
caller_line = caller[1]
|
||||
base._cache_digest_file_path = caller_line
|
||||
super
|
||||
end
|
||||
|
||||
def _cache_digest
|
||||
return @_cache_digest if defined?(@_cache_digest)
|
||||
@_cache_digest = digest_caller_file(_cache_digest_file_path)
|
||||
end
|
||||
|
||||
# Hashes contents of file for +_cache_digest+
|
||||
def digest_caller_file(caller_line)
|
||||
serializer_file_path = caller_line[CALLER_FILE]
|
||||
serializer_file_contents = IO.read(serializer_file_path)
|
||||
Digest::MD5.hexdigest(serializer_file_contents)
|
||||
rescue TypeError, Errno::ENOENT
|
||||
warn <<-EOF.strip_heredoc
|
||||
Cannot digest non-existent file: '#{caller_line}'.
|
||||
Please set `::_cache_digest` of the serializer
|
||||
if you'd like to cache it.
|
||||
EOF
|
||||
''.freeze
|
||||
end
|
||||
|
||||
def _skip_digest?
|
||||
_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) }
|
||||
non_cached = _attributes - cached
|
||||
non_cached = non_cached.map! { |field| _attributes_keys.fetch(field, field) }
|
||||
{
|
||||
cached: cached,
|
||||
non_cached: non_cached
|
||||
}
|
||||
end
|
||||
|
||||
# Enables a serializer to be automatically cached
|
||||
#
|
||||
# Sets +::_cache+ object to <tt>ActionController::Base.cache_store</tt>
|
||||
# when Rails.configuration.action_controller.perform_caching
|
||||
#
|
||||
# @param options [Hash] with valid keys:
|
||||
# cache_store : @see ::_cache
|
||||
# key : @see ::_cache_key
|
||||
# only : @see ::_cache_only
|
||||
# except : @see ::_cache_except
|
||||
# skip_digest : does not include digest in cache_key
|
||||
# all else : @see ::_cache_options
|
||||
#
|
||||
# @example
|
||||
# class PostSerializer < ActiveModel::Serializer
|
||||
# cache key: 'post', expires_in: 3.hours
|
||||
# attributes :title, :body
|
||||
#
|
||||
# has_many :comments
|
||||
# end
|
||||
#
|
||||
# @todo require less code comments. See
|
||||
# https://github.com/rails-api/active_model_serializers/pull/1249#issuecomment-146567837
|
||||
def cache(options = {})
|
||||
self._cache =
|
||||
options.delete(:cache_store) ||
|
||||
ActiveModelSerializers.config.cache_store ||
|
||||
ActiveSupport::Cache.lookup_store(:null_store)
|
||||
self._cache_key = options.delete(:key)
|
||||
self._cache_only = options.delete(:only)
|
||||
self._cache_except = options.delete(:except)
|
||||
self._cache_options = options.empty? ? nil : options
|
||||
end
|
||||
|
||||
# Value is from ActiveModelSerializers.config.perform_caching. Is used to
|
||||
# globally enable or disable all serializer caching, just like
|
||||
# Rails.configuration.action_controller.perform_caching, which is its
|
||||
# default value in a Rails application.
|
||||
# @return [true, false]
|
||||
# Memoizes value of config first time it is called with a non-nil value.
|
||||
# rubocop:disable Style/ClassVars
|
||||
def perform_caching
|
||||
return @@perform_caching if defined?(@@perform_caching) && !@@perform_caching.nil?
|
||||
@@perform_caching = ActiveModelSerializers.config.perform_caching
|
||||
end
|
||||
alias perform_caching? perform_caching
|
||||
# rubocop:enable Style/ClassVars
|
||||
|
||||
# The canonical method for getting the cache store for the serializer.
|
||||
#
|
||||
# @return [nil] when _cache is not set (i.e. when `cache` has not been called)
|
||||
# @return [._cache] when _cache is not the NullStore
|
||||
# @return [ActiveModelSerializers.config.cache_store] when _cache is the NullStore.
|
||||
# This is so we can use `cache` being called to mean the serializer should be cached
|
||||
# even if ActiveModelSerializers.config.cache_store has not yet been set.
|
||||
# That means that when _cache is the NullStore and ActiveModelSerializers.config.cache_store
|
||||
# is configured, `cache_store` becomes `ActiveModelSerializers.config.cache_store`.
|
||||
# @return [nil] when _cache is the NullStore and ActiveModelSerializers.config.cache_store is nil.
|
||||
def cache_store
|
||||
return nil if _cache.nil?
|
||||
return _cache if _cache.class != ActiveSupport::Cache::NullStore
|
||||
if ActiveModelSerializers.config.cache_store
|
||||
self._cache = ActiveModelSerializers.config.cache_store
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def cache_enabled?
|
||||
perform_caching? && cache_store && !_cache_only && !_cache_except
|
||||
end
|
||||
|
||||
def fragment_cache_enabled?
|
||||
perform_caching? && cache_store &&
|
||||
(_cache_only && !_cache_except || !_cache_only && _cache_except)
|
||||
end
|
||||
|
||||
# Read cache from cache_store
|
||||
# @return [Hash]
|
||||
def cache_read_multi(collection_serializer, adapter_instance, include_directive)
|
||||
return {} if ActiveModelSerializers.config.cache_store.blank?
|
||||
|
||||
keys = object_cache_keys(collection_serializer, adapter_instance, include_directive)
|
||||
|
||||
return {} if keys.blank?
|
||||
|
||||
ActiveModelSerializers.config.cache_store.read_multi(*keys)
|
||||
end
|
||||
|
||||
# Find all cache_key for the collection_serializer
|
||||
# @param serializers [ActiveModel::Serializer::CollectionSerializer]
|
||||
# @param adapter_instance [ActiveModelSerializers::Adapter::Base]
|
||||
# @param include_directive [JSONAPI::IncludeDirective]
|
||||
# @return [Array] all cache_key of collection_serializer
|
||||
def object_cache_keys(collection_serializer, adapter_instance, include_directive)
|
||||
cache_keys = []
|
||||
|
||||
collection_serializer.each do |serializer|
|
||||
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|
|
||||
cache_keys << object_cache_key(sub_serializer, adapter_instance)
|
||||
end
|
||||
else
|
||||
cache_keys << object_cache_key(association.serializer, adapter_instance)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
cache_keys.compact.uniq
|
||||
end
|
||||
|
||||
# @return [String, nil] the cache_key of the serializer or nil
|
||||
def object_cache_key(serializer, adapter_instance)
|
||||
return unless serializer.present? && serializer.object.present?
|
||||
|
||||
(serializer.class.cache_enabled? || serializer.class.fragment_cache_enabled?) ? serializer.cache_key(adapter_instance) : nil
|
||||
end
|
||||
end
|
||||
|
||||
### 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
|
||||
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)
|
||||
if serializer_class.cache_store
|
||||
serializer_class.cache_store.fetch(cache_key(adapter_instance), cache_options) do
|
||||
yield
|
||||
end
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
# 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
|
||||
fields = serializer_class.fragmented_attributes
|
||||
|
||||
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)
|
||||
|
||||
cached_fields = fields[:cached].dup
|
||||
key = cache_key(adapter_instance)
|
||||
cached_hash =
|
||||
cached_attributes.fetch(key) do
|
||||
serializer_class.cache_store.fetch(key, serializer_class._cache_options) do
|
||||
hash = attributes(cached_fields, true)
|
||||
include_directive = JSONAPI::IncludeDirective.new(cached_fields - hash.keys)
|
||||
hash.merge! resource_relationships({}, { include_directive: include_directive }, adapter_instance)
|
||||
end
|
||||
end
|
||||
# Merge both results
|
||||
adapter_instance.fragment_cache(cached_hash, non_cached_hash)
|
||||
end
|
||||
# rubocop:enable Metrics/AbcSize
|
||||
|
||||
def cache_key(adapter_instance)
|
||||
return @cache_key if defined?(@cache_key)
|
||||
|
||||
parts = []
|
||||
parts << object_cache_key
|
||||
parts << adapter_instance.cache_key
|
||||
parts << serializer_class._cache_digest unless serializer_class._skip_digest?
|
||||
@cache_key = expand_cache_key(parts)
|
||||
end
|
||||
|
||||
def expand_cache_key(parts)
|
||||
ActiveSupport::Cache.expand_cache_key(parts)
|
||||
end
|
||||
|
||||
# Use object's cache_key if available, else derive a key from the object
|
||||
# Pass the `key` option to the `cache` declaration or override this method to customize the cache key
|
||||
def object_cache_key
|
||||
if object.respond_to?(:cache_key)
|
||||
object.cache_key
|
||||
elsif (serializer_cache_key = (serializer_class._cache_key || serializer_class._cache_options[:key]))
|
||||
object_time_safe = object.updated_at
|
||||
object_time_safe = object_time_safe.strftime('%Y%m%d%H%M%S%9N') if object_time_safe.respond_to?(:strftime)
|
||||
"#{serializer_cache_key}/#{object.id}-#{object_time_safe}"
|
||||
else
|
||||
fail UndefinedCacheKey, "#{object.class} must define #cache_key, or the 'key:' option must be passed into '#{serializer_class}.cache'"
|
||||
end
|
||||
end
|
||||
|
||||
def serializer_class
|
||||
@serializer_class ||= self.class
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,14 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
class ErrorSerializer < ActiveModel::Serializer
|
||||
# @return [Hash<field_name,Array<error_message>>]
|
||||
def as_json
|
||||
object.errors.messages
|
||||
end
|
||||
|
||||
def success?
|
||||
false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,32 +0,0 @@
|
||||
require 'active_model/serializer/error_serializer'
|
||||
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
class ErrorsSerializer
|
||||
include Enumerable
|
||||
delegate :each, to: :@serializers
|
||||
attr_reader :object, :root
|
||||
|
||||
def initialize(resources, options = {})
|
||||
@root = options[:root]
|
||||
@object = resources
|
||||
@serializers = resources.map do |resource|
|
||||
serializer_class = options.fetch(:serializer) { ActiveModel::Serializer::ErrorSerializer }
|
||||
serializer_class.new(resource, options.except(:serializer))
|
||||
end
|
||||
end
|
||||
|
||||
def success?
|
||||
false
|
||||
end
|
||||
|
||||
def json_key
|
||||
nil
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :serializers
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,90 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
# Holds all the meta-data about a field (i.e. attribute or association) as it was
|
||||
# specified in the ActiveModel::Serializer class.
|
||||
# Notice that the field block is evaluated in the context of the serializer.
|
||||
Field = Struct.new(:name, :options, :block) do
|
||||
def initialize(*)
|
||||
super
|
||||
|
||||
validate_condition!
|
||||
end
|
||||
|
||||
# Compute the actual value of a field for a given serializer instance.
|
||||
# @param [Serializer] The serializer instance for which the value is computed.
|
||||
# @return [Object] value
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def value(serializer)
|
||||
if block
|
||||
serializer.instance_eval(&block)
|
||||
else
|
||||
serializer.read_attribute_for_serialization(name)
|
||||
end
|
||||
end
|
||||
|
||||
# Decide whether the field should be serialized by the given serializer instance.
|
||||
# @param [Serializer] The serializer instance
|
||||
# @return [Bool]
|
||||
#
|
||||
# @api private
|
||||
#
|
||||
def excluded?(serializer)
|
||||
case condition_type
|
||||
when :if
|
||||
!evaluate_condition(serializer)
|
||||
when :unless
|
||||
evaluate_condition(serializer)
|
||||
else
|
||||
false
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def validate_condition!
|
||||
return if condition_type == :none
|
||||
|
||||
case condition
|
||||
when Symbol, String, Proc
|
||||
# noop
|
||||
else
|
||||
fail TypeError, "#{condition_type.inspect} should be a Symbol, String or Proc"
|
||||
end
|
||||
end
|
||||
|
||||
def evaluate_condition(serializer)
|
||||
case condition
|
||||
when Symbol
|
||||
serializer.public_send(condition)
|
||||
when String
|
||||
serializer.instance_eval(condition)
|
||||
when Proc
|
||||
if condition.arity.zero?
|
||||
serializer.instance_exec(&condition)
|
||||
else
|
||||
serializer.instance_exec(serializer, &condition)
|
||||
end
|
||||
else
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def condition_type
|
||||
@condition_type ||=
|
||||
if options.key?(:if)
|
||||
:if
|
||||
elsif options.key?(:unless)
|
||||
:unless
|
||||
else
|
||||
:none
|
||||
end
|
||||
end
|
||||
|
||||
def condition
|
||||
options[condition_type]
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,31 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
class Fieldset
|
||||
def initialize(fields)
|
||||
@raw_fields = fields || {}
|
||||
end
|
||||
|
||||
def fields
|
||||
@fields ||= parsed_fields
|
||||
end
|
||||
|
||||
def fields_for(type)
|
||||
fields[type.singularize.to_sym] || fields[type.pluralize.to_sym]
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :raw_fields
|
||||
|
||||
private
|
||||
|
||||
def parsed_fields
|
||||
if raw_fields.is_a?(Hash)
|
||||
raw_fields.each_with_object({}) { |(k, v), h| h[k.to_sym] = v.map(&:to_sym) }
|
||||
else
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
# @api private
|
||||
class HasManyReflection < CollectionReflection
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
# @api private
|
||||
class HasOneReflection < SingularReflection
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,150 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
module Lint
|
||||
# == Active \Model \Serializer \Lint \Tests
|
||||
#
|
||||
# You can test whether an object is compliant with the Active \Model \Serializers
|
||||
# API by including <tt>ActiveModel::Serializer::Lint::Tests</tt> in your TestCase.
|
||||
# It will include tests that tell you whether your object is fully compliant,
|
||||
# or if not, which aspects of the API are not implemented.
|
||||
#
|
||||
# Note an object is not required to implement all APIs in order to work
|
||||
# with Active \Model \Serializers. This module only intends to provide guidance in case
|
||||
# you want all features out of the box.
|
||||
#
|
||||
# These tests do not attempt to determine the semantic correctness of the
|
||||
# returned values. For instance, you could implement <tt>serializable_hash</tt> to
|
||||
# always return +{}+, and the tests would pass. It is up to you to ensure
|
||||
# that the values are semantically meaningful.
|
||||
module Tests
|
||||
# Passes if the object responds to <tt>serializable_hash</tt> and if it takes
|
||||
# zero or one arguments.
|
||||
# Fails otherwise.
|
||||
#
|
||||
# <tt>serializable_hash</tt> returns a hash representation of a object's attributes.
|
||||
# Typically, it is implemented by including ActiveModel::Serialization.
|
||||
def test_serializable_hash
|
||||
assert_respond_to resource, :serializable_hash, 'The resource should respond to serializable_hash'
|
||||
resource.serializable_hash
|
||||
resource.serializable_hash(nil)
|
||||
end
|
||||
|
||||
# Passes if the object responds to <tt>read_attribute_for_serialization</tt>
|
||||
# and if it requires one argument (the attribute to be read).
|
||||
# Fails otherwise.
|
||||
#
|
||||
# <tt>read_attribute_for_serialization</tt> gets the attribute value for serialization
|
||||
# Typically, it is implemented by including ActiveModel::Serialization.
|
||||
def test_read_attribute_for_serialization
|
||||
assert_respond_to resource, :read_attribute_for_serialization, 'The resource should respond to read_attribute_for_serialization'
|
||||
actual_arity = resource.method(:read_attribute_for_serialization).arity
|
||||
# using absolute value since arity is:
|
||||
# 1 for def read_attribute_for_serialization(name); end
|
||||
# -1 for alias :read_attribute_for_serialization :send
|
||||
assert_equal 1, actual_arity.abs, "expected #{actual_arity.inspect}.abs to be 1 or -1"
|
||||
end
|
||||
|
||||
# Passes if the object responds to <tt>as_json</tt> and if it takes
|
||||
# zero or one arguments.
|
||||
# Fails otherwise.
|
||||
#
|
||||
# <tt>as_json</tt> returns a hash representation of a serialized object.
|
||||
# It may delegate to <tt>serializable_hash</tt>
|
||||
# Typically, it is implemented either by including ActiveModel::Serialization
|
||||
# which includes ActiveModel::Serializers::JSON.
|
||||
# or by the JSON gem when required.
|
||||
def test_as_json
|
||||
assert_respond_to resource, :as_json
|
||||
resource.as_json
|
||||
resource.as_json(nil)
|
||||
end
|
||||
|
||||
# Passes if the object responds to <tt>to_json</tt> and if it takes
|
||||
# zero or one arguments.
|
||||
# Fails otherwise.
|
||||
#
|
||||
# <tt>to_json</tt> returns a string representation (JSON) of a serialized object.
|
||||
# It may be called on the result of <tt>as_json</tt>.
|
||||
# Typically, it is implemented on all objects when the JSON gem is required.
|
||||
def test_to_json
|
||||
assert_respond_to resource, :to_json
|
||||
resource.to_json
|
||||
resource.to_json(nil)
|
||||
end
|
||||
|
||||
# Passes if the object responds to <tt>cache_key</tt>
|
||||
# Fails otherwise.
|
||||
#
|
||||
# <tt>cache_key</tt> returns a (self-expiring) unique key for the object,
|
||||
# and is part of the (self-expiring) cache_key, which is used by the
|
||||
# adapter. It is not required unless caching is enabled.
|
||||
def test_cache_key
|
||||
assert_respond_to resource, :cache_key
|
||||
actual_arity = resource.method(:cache_key).arity
|
||||
assert_includes [-1, 0], actual_arity, "expected #{actual_arity.inspect} to be 0 or -1"
|
||||
end
|
||||
|
||||
# Passes if the object responds to <tt>updated_at</tt> and if it takes no
|
||||
# arguments.
|
||||
# Fails otherwise.
|
||||
#
|
||||
# <tt>updated_at</tt> returns a Time object or iso8601 string and
|
||||
# is part of the (self-expiring) cache_key, which is used by the adapter.
|
||||
# It is not required unless caching is enabled.
|
||||
def test_updated_at
|
||||
assert_respond_to resource, :updated_at
|
||||
actual_arity = resource.method(:updated_at).arity
|
||||
assert_equal 0, actual_arity
|
||||
end
|
||||
|
||||
# Passes if the object responds to <tt>id</tt> and if it takes no
|
||||
# arguments.
|
||||
# Fails otherwise.
|
||||
#
|
||||
# <tt>id</tt> returns a unique identifier for the object.
|
||||
# It is not required unless caching is enabled.
|
||||
def test_id
|
||||
assert_respond_to resource, :id
|
||||
assert_equal 0, resource.method(:id).arity
|
||||
end
|
||||
|
||||
# Passes if the object's class responds to <tt>model_name</tt> and if it
|
||||
# is in an instance of +ActiveModel::Name+.
|
||||
# Fails otherwise.
|
||||
#
|
||||
# <tt>model_name</tt> returns an ActiveModel::Name instance.
|
||||
# It is used by the serializer to identify the object's type.
|
||||
# It is not required unless caching is enabled.
|
||||
def test_model_name
|
||||
resource_class = resource.class
|
||||
assert_respond_to resource_class, :model_name
|
||||
assert_instance_of resource_class.model_name, ActiveModel::Name
|
||||
end
|
||||
|
||||
def test_active_model_errors
|
||||
assert_respond_to resource, :errors
|
||||
end
|
||||
|
||||
def test_active_model_errors_human_attribute_name
|
||||
assert_respond_to resource.class, :human_attribute_name
|
||||
assert_equal(-2, resource.class.method(:human_attribute_name).arity)
|
||||
end
|
||||
|
||||
def test_active_model_errors_lookup_ancestors
|
||||
assert_respond_to resource.class, :lookup_ancestors
|
||||
assert_equal 0, resource.class.method(:lookup_ancestors).arity
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def resource
|
||||
@resource or fail "'@resource' must be set as the linted object"
|
||||
end
|
||||
|
||||
def assert_instance_of(result, name)
|
||||
assert result.instance_of?(name), "#{result} should be an instance of #{name}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,17 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
class Null < Serializer
|
||||
def attributes(*)
|
||||
{}
|
||||
end
|
||||
|
||||
def associations(*)
|
||||
{}
|
||||
end
|
||||
|
||||
def serializable_hash(*)
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,163 +0,0 @@
|
||||
require 'active_model/serializer/field'
|
||||
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
# Holds all the meta-data about an association as it was specified in the
|
||||
# ActiveModel::Serializer class.
|
||||
#
|
||||
# @example
|
||||
# class PostSerializer < ActiveModel::Serializer
|
||||
# has_one :author, serializer: AuthorSerializer
|
||||
# has_many :comments
|
||||
# has_many :comments, key: :last_comments do
|
||||
# object.comments.last(1)
|
||||
# end
|
||||
# has_many :secret_meta_data, if: :is_admin?
|
||||
#
|
||||
# def is_admin?
|
||||
# current_user.admin?
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Specifically, the association 'comments' is evaluated two different ways:
|
||||
# 1) as 'comments' and named 'comments'.
|
||||
# 2) as 'object.comments.last(1)' and named 'last_comments'.
|
||||
#
|
||||
# PostSerializer._reflections #=>
|
||||
# # [
|
||||
# # HasOneReflection.new(:author, serializer: AuthorSerializer),
|
||||
# # HasManyReflection.new(:comments)
|
||||
# # HasManyReflection.new(:comments, { key: :last_comments }, #<Block>)
|
||||
# # HasManyReflection.new(:secret_meta_data, { if: :is_admin? })
|
||||
# # ]
|
||||
#
|
||||
# So you can inspect reflections in your Adapters.
|
||||
#
|
||||
class Reflection < Field
|
||||
def initialize(*)
|
||||
super
|
||||
@_links = {}
|
||||
@_include_data = Serializer.config.include_data_default
|
||||
@_meta = nil
|
||||
end
|
||||
|
||||
def link(name, value = nil, &block)
|
||||
@_links[name] = block || value
|
||||
:nil
|
||||
end
|
||||
|
||||
def meta(value = nil, &block)
|
||||
@_meta = block || value
|
||||
:nil
|
||||
end
|
||||
|
||||
def include_data(value = true)
|
||||
@_include_data = 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
|
||||
|
||||
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.
|
||||
#
|
||||
# @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 = options.dup
|
||||
|
||||
# Pass the parent's namespace onto the child serializer
|
||||
reflection_options[:namespace] ||= parent_serializer_options[:namespace]
|
||||
|
||||
association_value = value(parent_serializer, include_slice)
|
||||
serializer_class = parent_serializer.class.serializer_for(association_value, reflection_options)
|
||||
reflection_options[:include_data] = include_data?(include_slice)
|
||||
reflection_options[:links] = @_links
|
||||
reflection_options[:meta] = @_meta
|
||||
|
||||
if serializer_class
|
||||
serializer = catch(:no_serializer) do
|
||||
serializer_class.new(
|
||||
association_value,
|
||||
serializer_options(parent_serializer, parent_serializer_options, reflection_options)
|
||||
)
|
||||
end
|
||||
if serializer.nil?
|
||||
reflection_options[:virtual_value] = association_value.try(:as_json) || association_value
|
||||
else
|
||||
reflection_options[:serializer] = serializer
|
||||
end
|
||||
elsif !association_value.nil? && !association_value.instance_of?(Object)
|
||||
reflection_options[:virtual_value] = association_value
|
||||
end
|
||||
|
||||
block = nil
|
||||
Association.new(name, reflection_options, block)
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_accessor :object, :scope
|
||||
|
||||
private
|
||||
|
||||
def include_data?(include_slice)
|
||||
if @_include_data == :if_sideloaded
|
||||
include_slice.key?(name)
|
||||
else
|
||||
@_include_data
|
||||
end
|
||||
end
|
||||
|
||||
def serializer_options(parent_serializer, parent_serializer_options, reflection_options)
|
||||
serializer = reflection_options.fetch(:serializer, nil)
|
||||
|
||||
serializer_options = parent_serializer_options.except(:serializer)
|
||||
serializer_options[:serializer] = serializer if serializer
|
||||
serializer_options[:serializer_context_class] = parent_serializer.class
|
||||
serializer_options
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
# @api private
|
||||
class SingularReflection < Reflection
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,5 +0,0 @@
|
||||
module ActiveModel
|
||||
class Serializer
|
||||
VERSION = '0.10.5'.freeze
|
||||
end
|
||||
end
|
||||
@@ -1,53 +0,0 @@
|
||||
require 'active_model'
|
||||
require 'active_support'
|
||||
require 'active_support/core_ext/object/with_options'
|
||||
require 'active_support/core_ext/string/inflections'
|
||||
require 'active_support/json'
|
||||
module ActiveModelSerializers
|
||||
extend ActiveSupport::Autoload
|
||||
autoload :Model
|
||||
autoload :Callbacks
|
||||
autoload :Deserialization
|
||||
autoload :SerializableResource
|
||||
autoload :Logging
|
||||
autoload :Test
|
||||
autoload :Adapter
|
||||
autoload :JsonPointer
|
||||
autoload :Deprecate
|
||||
autoload :LookupChain
|
||||
|
||||
class << self; attr_accessor :logger; end
|
||||
self.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
|
||||
|
||||
def self.config
|
||||
ActiveModel::Serializer.config
|
||||
end
|
||||
|
||||
# The file name and line number of the caller of the caller of this method.
|
||||
def self.location_of_caller
|
||||
caller[1] =~ /(.*?):(\d+).*?$/i
|
||||
file = Regexp.last_match(1)
|
||||
lineno = Regexp.last_match(2).to_i
|
||||
|
||||
[file, lineno]
|
||||
end
|
||||
|
||||
# Memoized default include directive
|
||||
# @return [JSONAPI::IncludeDirective]
|
||||
def self.default_include_directive
|
||||
@default_include_directive ||= JSONAPI::IncludeDirective.new(config.default_includes, allow_wildcard: true)
|
||||
end
|
||||
|
||||
def self.silence_warnings
|
||||
original_verbose = $VERBOSE
|
||||
$VERBOSE = nil
|
||||
yield
|
||||
ensure
|
||||
$VERBOSE = original_verbose
|
||||
end
|
||||
|
||||
require 'active_model/serializer/version'
|
||||
require 'active_model/serializer'
|
||||
require 'active_model/serializable_resource'
|
||||
require 'active_model_serializers/railtie' if defined?(::Rails)
|
||||
end
|
||||
@@ -1,98 +0,0 @@
|
||||
module ActiveModelSerializers
|
||||
module Adapter
|
||||
UnknownAdapterError = Class.new(ArgumentError)
|
||||
ADAPTER_MAP = {} # rubocop:disable Style/MutableConstant
|
||||
private_constant :ADAPTER_MAP if defined?(private_constant)
|
||||
|
||||
class << self # All methods are class functions
|
||||
# :nocov:
|
||||
def new(*args)
|
||||
fail ArgumentError, 'Adapters inherit from Adapter::Base.' \
|
||||
"Adapter.new called with args: '#{args.inspect}', from" \
|
||||
"'caller[0]'."
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
def configured_adapter
|
||||
lookup(ActiveModelSerializers.config.adapter)
|
||||
end
|
||||
|
||||
def create(resource, options = {})
|
||||
override = options.delete(:adapter)
|
||||
klass = override ? adapter_class(override) : configured_adapter
|
||||
klass.new(resource, options)
|
||||
end
|
||||
|
||||
# @see ActiveModelSerializers::Adapter.lookup
|
||||
def adapter_class(adapter)
|
||||
ActiveModelSerializers::Adapter.lookup(adapter)
|
||||
end
|
||||
|
||||
# @return [Hash<adapter_name, adapter_class>]
|
||||
def adapter_map
|
||||
ADAPTER_MAP
|
||||
end
|
||||
|
||||
# @return [Array<Symbol>] list of adapter names
|
||||
def adapters
|
||||
adapter_map.keys.sort
|
||||
end
|
||||
|
||||
# Adds an adapter 'klass' with 'name' to the 'adapter_map'
|
||||
# Names are stringified and underscored
|
||||
# @param name [Symbol, String, Class] name of the registered adapter
|
||||
# @param klass [Class] adapter class itself, optional if name is the class
|
||||
# @example
|
||||
# AMS::Adapter.register(:my_adapter, MyAdapter)
|
||||
# @note The registered name strips out 'ActiveModelSerializers::Adapter::'
|
||||
# so that registering 'ActiveModelSerializers::Adapter::Json' and
|
||||
# 'Json' will both register as 'json'.
|
||||
def register(name, klass = name)
|
||||
name = name.to_s.gsub(/\AActiveModelSerializers::Adapter::/, ''.freeze)
|
||||
adapter_map[name.underscore] = klass
|
||||
self
|
||||
end
|
||||
|
||||
def registered_name(adapter_class)
|
||||
ADAPTER_MAP.key adapter_class
|
||||
end
|
||||
|
||||
# @param adapter [String, Symbol, Class] name to fetch adapter by
|
||||
# @return [ActiveModelSerializers::Adapter] subclass of Adapter
|
||||
# @raise [UnknownAdapterError]
|
||||
def lookup(adapter)
|
||||
# 1. return if is a class
|
||||
return adapter if adapter.is_a?(Class)
|
||||
adapter_name = adapter.to_s.underscore
|
||||
# 2. return if registered
|
||||
adapter_map.fetch(adapter_name) do
|
||||
# 3. try to find adapter class from environment
|
||||
adapter_class = find_by_name(adapter_name)
|
||||
register(adapter_name, adapter_class)
|
||||
adapter_class
|
||||
end
|
||||
rescue NameError, ArgumentError => e
|
||||
failure_message =
|
||||
"NameError: #{e.message}. Unknown adapter: #{adapter.inspect}. Valid adapters are: #{adapters}"
|
||||
raise UnknownAdapterError, failure_message, e.backtrace
|
||||
end
|
||||
|
||||
# @api private
|
||||
def find_by_name(adapter_name)
|
||||
adapter_name = adapter_name.to_s.classify.tr('API', 'Api')
|
||||
"ActiveModelSerializers::Adapter::#{adapter_name}".safe_constantize ||
|
||||
"ActiveModelSerializers::Adapter::#{adapter_name.pluralize}".safe_constantize or # rubocop:disable Style/AndOr
|
||||
fail UnknownAdapterError
|
||||
end
|
||||
private :find_by_name
|
||||
end
|
||||
|
||||
# Gotta be at the bottom to use the code above it :(
|
||||
extend ActiveSupport::Autoload
|
||||
autoload :Base
|
||||
autoload :Null
|
||||
autoload :Attributes
|
||||
autoload :Json
|
||||
autoload :JsonApi
|
||||
end
|
||||
end
|
||||
@@ -1,13 +0,0 @@
|
||||
module ActiveModelSerializers
|
||||
module Adapter
|
||||
class Attributes < Base
|
||||
def serializable_hash(options = nil)
|
||||
options = serialization_options(options)
|
||||
options[:fields] ||= instance_options[:fields]
|
||||
serialized_hash = serializer.serializable_hash(instance_options, options, self)
|
||||
|
||||
self.class.transform_key_casing!(serialized_hash, instance_options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,83 +0,0 @@
|
||||
require 'case_transform'
|
||||
|
||||
module ActiveModelSerializers
|
||||
module Adapter
|
||||
class Base
|
||||
# Automatically register adapters when subclassing
|
||||
def self.inherited(subclass)
|
||||
ActiveModelSerializers::Adapter.register(subclass)
|
||||
end
|
||||
|
||||
# Sets the default transform for the adapter.
|
||||
#
|
||||
# @return [Symbol] the default transform for the adapter
|
||||
def self.default_key_transform
|
||||
:unaltered
|
||||
end
|
||||
|
||||
# Determines the transform to use in order of precedence:
|
||||
# adapter option, global config, adapter default.
|
||||
#
|
||||
# @param options [Object]
|
||||
# @return [Symbol] the transform to use
|
||||
def self.transform(options)
|
||||
return options[:key_transform] if options && options[:key_transform]
|
||||
ActiveModelSerializers.config.key_transform || default_key_transform
|
||||
end
|
||||
|
||||
# Transforms the casing of the supplied value.
|
||||
#
|
||||
# @param value [Object] the value to be transformed
|
||||
# @param options [Object] serializable resource options
|
||||
# @return [Symbol] the default transform for the adapter
|
||||
def self.transform_key_casing!(value, options)
|
||||
CaseTransform.send(transform(options), value)
|
||||
end
|
||||
|
||||
def self.cache_key
|
||||
@cache_key ||= ActiveModelSerializers::Adapter.registered_name(self)
|
||||
end
|
||||
|
||||
def self.fragment_cache(cached_hash, non_cached_hash)
|
||||
non_cached_hash.merge cached_hash
|
||||
end
|
||||
|
||||
attr_reader :serializer, :instance_options
|
||||
|
||||
def initialize(serializer, options = {})
|
||||
@serializer = serializer
|
||||
@instance_options = options
|
||||
end
|
||||
|
||||
# Subclasses that implement this method must first call
|
||||
# options = serialization_options(options)
|
||||
def serializable_hash(_options = nil)
|
||||
fail NotImplementedError, 'This is an abstract method. Should be implemented at the concrete adapter.'
|
||||
end
|
||||
|
||||
def as_json(options = nil)
|
||||
serializable_hash(options)
|
||||
end
|
||||
|
||||
def cache_key
|
||||
self.class.cache_key
|
||||
end
|
||||
|
||||
def fragment_cache(cached_hash, non_cached_hash)
|
||||
self.class.fragment_cache(cached_hash, non_cached_hash)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# see https://github.com/rails-api/active_model_serializers/pull/965
|
||||
# When <tt>options</tt> is +nil+, sets it to +{}+
|
||||
def serialization_options(options)
|
||||
options ||= {} # rubocop:disable Lint/UselessAssignment
|
||||
end
|
||||
|
||||
def root
|
||||
serializer.json_key.to_sym if serializer.json_key
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,21 +0,0 @@
|
||||
module ActiveModelSerializers
|
||||
module Adapter
|
||||
class Json < Base
|
||||
def serializable_hash(options = nil)
|
||||
options = serialization_options(options)
|
||||
serialized_hash = { root => Attributes.new(serializer, instance_options).serializable_hash(options) }
|
||||
serialized_hash[meta_key] = meta unless meta.blank?
|
||||
|
||||
self.class.transform_key_casing!(serialized_hash, instance_options)
|
||||
end
|
||||
|
||||
def meta
|
||||
instance_options.fetch(:meta, nil)
|
||||
end
|
||||
|
||||
def meta_key
|
||||
instance_options.fetch(:meta_key, 'meta'.freeze)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,517 +0,0 @@
|
||||
# {http://jsonapi.org/format/ JSON API specification}
|
||||
# rubocop:disable Style/AsciiComments
|
||||
# TODO: implement!
|
||||
# ☐ https://github.com/rails-api/active_model_serializers/issues/1235
|
||||
# TODO: use uri_template in link generation?
|
||||
# ☐ https://github.com/rails-api/active_model_serializers/pull/1282#discussion_r42528812
|
||||
# see gem https://github.com/hannesg/uri_template
|
||||
# spec http://tools.ietf.org/html/rfc6570
|
||||
# impl https://developer.github.com/v3/#schema https://api.github.com/
|
||||
# TODO: validate against a JSON schema document?
|
||||
# ☐ https://github.com/rails-api/active_model_serializers/issues/1162
|
||||
# ☑ https://github.com/rails-api/active_model_serializers/pull/1270
|
||||
# TODO: Routing
|
||||
# ☐ https://github.com/rails-api/active_model_serializers/pull/1476
|
||||
# TODO: Query Params
|
||||
# ☑ `include` https://github.com/rails-api/active_model_serializers/pull/1131
|
||||
# ☑ `fields` https://github.com/rails-api/active_model_serializers/pull/700
|
||||
# ☑ `page[number]=3&page[size]=1` https://github.com/rails-api/active_model_serializers/pull/1041
|
||||
# ☐ `filter`
|
||||
# ☐ `sort`
|
||||
module ActiveModelSerializers
|
||||
module Adapter
|
||||
class JsonApi < Base
|
||||
extend ActiveSupport::Autoload
|
||||
autoload :Jsonapi
|
||||
autoload :ResourceIdentifier
|
||||
autoload :Relationship
|
||||
autoload :Link
|
||||
autoload :PaginationLinks
|
||||
autoload :Meta
|
||||
autoload :Error
|
||||
autoload :Deserialization
|
||||
|
||||
def self.default_key_transform
|
||||
:dash
|
||||
end
|
||||
|
||||
def self.fragment_cache(cached_hash, non_cached_hash, root = true)
|
||||
core_cached = cached_hash.first
|
||||
core_non_cached = non_cached_hash.first
|
||||
no_root_cache = cached_hash.delete_if { |key, _value| key == core_cached[0] }
|
||||
no_root_non_cache = non_cached_hash.delete_if { |key, _value| key == core_non_cached[0] }
|
||||
cached_resource = (core_cached[1]) ? core_cached[1].deep_merge(core_non_cached[1]) : core_non_cached[1]
|
||||
hash = root ? { root => cached_resource } : cached_resource
|
||||
|
||||
hash.deep_merge no_root_non_cache.deep_merge no_root_cache
|
||||
end
|
||||
|
||||
def initialize(serializer, options = {})
|
||||
super
|
||||
@include_directive = JSONAPI::IncludeDirective.new(options[:include], allow_wildcard: true)
|
||||
@fieldset = options[:fieldset] || ActiveModel::Serializer::Fieldset.new(options.delete(:fields))
|
||||
end
|
||||
|
||||
# {http://jsonapi.org/format/#crud Requests are transactional, i.e. success or failure}
|
||||
# {http://jsonapi.org/format/#document-top-level data and errors MUST NOT coexist in the same document.}
|
||||
def serializable_hash(*)
|
||||
document = if serializer.success?
|
||||
success_document
|
||||
else
|
||||
failure_document
|
||||
end
|
||||
self.class.transform_key_casing!(document, instance_options)
|
||||
end
|
||||
|
||||
def fragment_cache(cached_hash, non_cached_hash)
|
||||
root = !instance_options.include?(:include)
|
||||
self.class.fragment_cache(cached_hash, non_cached_hash, root)
|
||||
end
|
||||
|
||||
# {http://jsonapi.org/format/#document-top-level Primary data}
|
||||
# definition:
|
||||
# ☐ toplevel_data (required)
|
||||
# ☐ toplevel_included
|
||||
# ☑ toplevel_meta
|
||||
# ☑ toplevel_links
|
||||
# ☑ toplevel_jsonapi
|
||||
# structure:
|
||||
# {
|
||||
# data: toplevel_data,
|
||||
# included: toplevel_included,
|
||||
# meta: toplevel_meta,
|
||||
# links: toplevel_links,
|
||||
# jsonapi: toplevel_jsonapi
|
||||
# }.reject! {|_,v| v.nil? }
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
def success_document
|
||||
is_collection = serializer.respond_to?(:each)
|
||||
serializers = is_collection ? serializer : [serializer]
|
||||
primary_data, included = resource_objects_for(serializers)
|
||||
|
||||
hash = {}
|
||||
# toplevel_data
|
||||
# definition:
|
||||
# oneOf
|
||||
# resource
|
||||
# array of unique items of type 'resource'
|
||||
# null
|
||||
#
|
||||
# description:
|
||||
# The document's "primary data" is a representation of the resource or collection of resources
|
||||
# targeted by a request.
|
||||
#
|
||||
# Singular: the resource object.
|
||||
#
|
||||
# Collection: one of an array of resource objects, an array of resource identifier objects, or
|
||||
# an empty array ([]), for requests that target resource collections.
|
||||
#
|
||||
# None: null if the request is one that might correspond to a single resource, but doesn't currently.
|
||||
# structure:
|
||||
# if serializable_resource.resource?
|
||||
# resource
|
||||
# elsif serializable_resource.collection?
|
||||
# [
|
||||
# resource,
|
||||
# resource
|
||||
# ]
|
||||
# else
|
||||
# nil
|
||||
# end
|
||||
hash[:data] = is_collection ? primary_data : primary_data[0]
|
||||
# toplevel_included
|
||||
# alias included
|
||||
# definition:
|
||||
# array of unique items of type 'resource'
|
||||
#
|
||||
# description:
|
||||
# To reduce the number of HTTP requests, servers **MAY** allow
|
||||
# responses that include related resources along with the requested primary
|
||||
# resources. Such responses are called "compound documents".
|
||||
# structure:
|
||||
# [
|
||||
# resource,
|
||||
# resource
|
||||
# ]
|
||||
hash[:included] = included if included.any?
|
||||
|
||||
Jsonapi.add!(hash)
|
||||
|
||||
if instance_options[:links]
|
||||
hash[:links] ||= {}
|
||||
hash[:links].update(instance_options[:links])
|
||||
end
|
||||
|
||||
if is_collection && serializer.paginated?
|
||||
hash[:links] ||= {}
|
||||
hash[:links].update(pagination_links_for(serializer))
|
||||
end
|
||||
|
||||
hash[:meta] = instance_options[:meta] unless instance_options[:meta].blank?
|
||||
|
||||
hash
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
|
||||
# {http://jsonapi.org/format/#errors JSON API Errors}
|
||||
# TODO: look into caching
|
||||
# definition:
|
||||
# ☑ toplevel_errors array (required)
|
||||
# ☐ toplevel_meta
|
||||
# ☐ toplevel_jsonapi
|
||||
# structure:
|
||||
# {
|
||||
# errors: toplevel_errors,
|
||||
# meta: toplevel_meta,
|
||||
# jsonapi: toplevel_jsonapi
|
||||
# }.reject! {|_,v| v.nil? }
|
||||
# prs:
|
||||
# https://github.com/rails-api/active_model_serializers/pull/1004
|
||||
def failure_document
|
||||
hash = {}
|
||||
# PR Please :)
|
||||
# Jsonapi.add!(hash)
|
||||
|
||||
# toplevel_errors
|
||||
# definition:
|
||||
# array of unique items of type 'error'
|
||||
# structure:
|
||||
# [
|
||||
# error,
|
||||
# error
|
||||
# ]
|
||||
if serializer.respond_to?(:each)
|
||||
hash[:errors] = serializer.flat_map do |error_serializer|
|
||||
Error.resource_errors(error_serializer, instance_options)
|
||||
end
|
||||
else
|
||||
hash[:errors] = Error.resource_errors(serializer, instance_options)
|
||||
end
|
||||
hash
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :fieldset
|
||||
|
||||
private
|
||||
|
||||
# {http://jsonapi.org/format/#document-resource-objects Primary data}
|
||||
# resource
|
||||
# definition:
|
||||
# JSON Object
|
||||
#
|
||||
# properties:
|
||||
# type (required) : String
|
||||
# id (required) : String
|
||||
# attributes
|
||||
# relationships
|
||||
# links
|
||||
# meta
|
||||
#
|
||||
# description:
|
||||
# "Resource objects" appear in a JSON API document to represent resources
|
||||
# structure:
|
||||
# {
|
||||
# type: 'admin--some-user',
|
||||
# id: '1336',
|
||||
# attributes: attributes,
|
||||
# relationships: relationships,
|
||||
# links: links,
|
||||
# meta: meta,
|
||||
# }.reject! {|_,v| v.nil? }
|
||||
# prs:
|
||||
# type
|
||||
# https://github.com/rails-api/active_model_serializers/pull/1122
|
||||
# [x] https://github.com/rails-api/active_model_serializers/pull/1213
|
||||
# https://github.com/rails-api/active_model_serializers/pull/1216
|
||||
# https://github.com/rails-api/active_model_serializers/pull/1029
|
||||
# links
|
||||
# [x] https://github.com/rails-api/active_model_serializers/pull/1246
|
||||
# [x] url helpers https://github.com/rails-api/active_model_serializers/issues/1269
|
||||
# meta
|
||||
# [x] https://github.com/rails-api/active_model_serializers/pull/1340
|
||||
def resource_objects_for(serializers)
|
||||
@primary = []
|
||||
@included = []
|
||||
@resource_identifiers = Set.new
|
||||
serializers.each { |serializer| process_resource(serializer, true, @include_directive) }
|
||||
serializers.each { |serializer| process_relationships(serializer, @include_directive) }
|
||||
|
||||
[@primary, @included]
|
||||
end
|
||||
|
||||
def process_resource(serializer, primary, include_slice = {})
|
||||
resource_identifier = ResourceIdentifier.new(serializer, instance_options).as_json
|
||||
return false unless @resource_identifiers.add?(resource_identifier)
|
||||
|
||||
resource_object = resource_object_for(serializer, include_slice)
|
||||
if primary
|
||||
@primary << resource_object
|
||||
else
|
||||
@included << resource_object
|
||||
end
|
||||
|
||||
true
|
||||
end
|
||||
|
||||
def process_relationships(serializer, include_slice)
|
||||
serializer.associations(include_slice).each do |association|
|
||||
process_relationship(association.serializer, include_slice[association.key])
|
||||
end
|
||||
end
|
||||
|
||||
def process_relationship(serializer, include_slice)
|
||||
if serializer.respond_to?(:each)
|
||||
serializer.each { |s| process_relationship(s, include_slice) }
|
||||
return
|
||||
end
|
||||
return unless serializer && serializer.object
|
||||
return unless process_resource(serializer, false, include_slice)
|
||||
|
||||
process_relationships(serializer, include_slice)
|
||||
end
|
||||
|
||||
# {http://jsonapi.org/format/#document-resource-object-attributes Document Resource Object Attributes}
|
||||
# attributes
|
||||
# definition:
|
||||
# JSON Object
|
||||
#
|
||||
# patternProperties:
|
||||
# ^(?!relationships$|links$)\\w[-\\w_]*$
|
||||
#
|
||||
# description:
|
||||
# Members of the attributes object ("attributes") represent information about the resource
|
||||
# object in which it's defined.
|
||||
# Attributes may contain any valid JSON value
|
||||
# structure:
|
||||
# {
|
||||
# foo: 'bar'
|
||||
# }
|
||||
def attributes_for(serializer, fields)
|
||||
serializer.attributes(fields).except(:id)
|
||||
end
|
||||
|
||||
# {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
|
||||
|
||||
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
|
||||
# ☐ links
|
||||
# ☐ pagination
|
||||
#
|
||||
# description:
|
||||
# Link members related to the primary data.
|
||||
# structure:
|
||||
# links.merge!(pagination)
|
||||
# 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?
|
||||
|
||||
# toplevel_meta
|
||||
# alias meta
|
||||
# definition:
|
||||
# meta
|
||||
# structure
|
||||
# {
|
||||
# :'git-ref' => 'abc123'
|
||||
# }
|
||||
meta = meta_for(serializer)
|
||||
resource_object[:meta] = meta unless meta.blank?
|
||||
|
||||
resource_object
|
||||
end
|
||||
|
||||
# {http://jsonapi.org/format/#document-resource-object-relationships Document Resource Object Relationship}
|
||||
# relationships
|
||||
# definition:
|
||||
# JSON Object
|
||||
#
|
||||
# patternProperties:
|
||||
# ^\\w[-\\w_]*$"
|
||||
#
|
||||
# properties:
|
||||
# data : relationshipsData
|
||||
# links
|
||||
# meta
|
||||
#
|
||||
# description:
|
||||
#
|
||||
# Members of the relationships object ("relationships") represent references from the
|
||||
# resource object in which it's defined to other resource objects."
|
||||
# structure:
|
||||
# {
|
||||
# links: links,
|
||||
# meta: meta,
|
||||
# data: relationshipsData
|
||||
# }.reject! {|_,v| v.nil? }
|
||||
#
|
||||
# prs:
|
||||
# links
|
||||
# [x] https://github.com/rails-api/active_model_serializers/pull/1454
|
||||
# meta
|
||||
# [x] https://github.com/rails-api/active_model_serializers/pull/1454
|
||||
# polymorphic
|
||||
# [ ] https://github.com/rails-api/active_model_serializers/pull/1420
|
||||
#
|
||||
# relationshipsData
|
||||
# definition:
|
||||
# oneOf
|
||||
# relationshipToOne
|
||||
# relationshipToMany
|
||||
#
|
||||
# description:
|
||||
# Member, whose value represents "resource linkage"
|
||||
# structure:
|
||||
# if has_one?
|
||||
# relationshipToOne
|
||||
# else
|
||||
# relationshipToMany
|
||||
# end
|
||||
#
|
||||
# definition:
|
||||
# anyOf
|
||||
# null
|
||||
# linkage
|
||||
#
|
||||
# relationshipToOne
|
||||
# description:
|
||||
#
|
||||
# References to other resource objects in a to-one ("relationship"). Relationships can be
|
||||
# specified by including a member in a resource's links object.
|
||||
#
|
||||
# None: Describes an empty to-one relationship.
|
||||
# structure:
|
||||
# if has_related?
|
||||
# linkage
|
||||
# else
|
||||
# nil
|
||||
# end
|
||||
#
|
||||
# relationshipToMany
|
||||
# definition:
|
||||
# array of unique items of type 'linkage'
|
||||
#
|
||||
# description:
|
||||
# An array of objects each containing "type" and "id" members for to-many relationships
|
||||
# structure:
|
||||
# [
|
||||
# linkage,
|
||||
# linkage
|
||||
# ]
|
||||
# prs:
|
||||
# polymorphic
|
||||
# [ ] https://github.com/rails-api/active_model_serializers/pull/1282
|
||||
#
|
||||
# linkage
|
||||
# definition:
|
||||
# type (required) : String
|
||||
# id (required) : String
|
||||
# meta
|
||||
#
|
||||
# description:
|
||||
# The "type" and "id" to non-empty members.
|
||||
# structure:
|
||||
# {
|
||||
# type: 'required-type',
|
||||
# id: 'required-id',
|
||||
# meta: meta
|
||||
# }.reject! {|_,v| v.nil? }
|
||||
def relationships_for(serializer, requested_associations, include_slice)
|
||||
include_directive = JSONAPI::IncludeDirective.new(
|
||||
requested_associations,
|
||||
allow_wildcard: true
|
||||
)
|
||||
serializer.associations(include_directive, include_slice).each_with_object({}) do |association, hash|
|
||||
hash[association.key] = Relationship.new(serializer, instance_options, association).as_json
|
||||
end
|
||||
end
|
||||
|
||||
# {http://jsonapi.org/format/#document-links Document Links}
|
||||
# links
|
||||
# definition:
|
||||
# JSON Object
|
||||
#
|
||||
# properties:
|
||||
# self : URI
|
||||
# related : link
|
||||
#
|
||||
# description:
|
||||
# A resource object **MAY** contain references to other resource objects ("relationships").
|
||||
# Relationships may be to-one or to-many. Relationships can be specified by including a member
|
||||
# in a resource's links object.
|
||||
#
|
||||
# A `self` member’s value is a URL for the relationship itself (a "relationship URL"). This
|
||||
# URL allows the client to directly manipulate the relationship. For example, it would allow
|
||||
# a client to remove an `author` from an `article` without deleting the people resource
|
||||
# itself.
|
||||
# structure:
|
||||
# {
|
||||
# self: 'http://example.com/etc',
|
||||
# related: link
|
||||
# }.reject! {|_,v| v.nil? }
|
||||
def links_for(serializer)
|
||||
serializer._links.each_with_object({}) do |(name, value), hash|
|
||||
result = Link.new(serializer, value).as_json
|
||||
hash[name] = result if result
|
||||
end
|
||||
end
|
||||
|
||||
# {http://jsonapi.org/format/#fetching-pagination Pagination Links}
|
||||
# pagination
|
||||
# definition:
|
||||
# first : pageObject
|
||||
# last : pageObject
|
||||
# prev : pageObject
|
||||
# next : pageObject
|
||||
# structure:
|
||||
# {
|
||||
# first: pageObject,
|
||||
# last: pageObject,
|
||||
# prev: pageObject,
|
||||
# next: pageObject
|
||||
# }
|
||||
#
|
||||
# pageObject
|
||||
# definition:
|
||||
# oneOf
|
||||
# URI
|
||||
# null
|
||||
#
|
||||
# description:
|
||||
# The <x> page of data
|
||||
# structure:
|
||||
# if has_page?
|
||||
# 'http://example.com/some-page?page[number][x]'
|
||||
# else
|
||||
# nil
|
||||
# end
|
||||
# prs:
|
||||
# https://github.com/rails-api/active_model_serializers/pull/1041
|
||||
def pagination_links_for(serializer)
|
||||
PaginationLinks.new(serializer.object, instance_options).as_json
|
||||
end
|
||||
|
||||
# {http://jsonapi.org/format/#document-meta Docment Meta}
|
||||
def meta_for(serializer)
|
||||
Meta.new(serializer).as_json
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Style/AsciiComments
|
||||
@@ -1,213 +0,0 @@
|
||||
module ActiveModelSerializers
|
||||
module Adapter
|
||||
class JsonApi
|
||||
# NOTE(Experimental):
|
||||
# This is an experimental feature. Both the interface and internals could be subject
|
||||
# to changes.
|
||||
module Deserialization
|
||||
InvalidDocument = Class.new(ArgumentError)
|
||||
|
||||
module_function
|
||||
|
||||
# Transform a JSON API document, containing a single data object,
|
||||
# into a hash that is ready for ActiveRecord::Base.new() and such.
|
||||
# Raises InvalidDocument if the payload is not properly formatted.
|
||||
#
|
||||
# @param [Hash|ActionController::Parameters] document
|
||||
# @param [Hash] options
|
||||
# only: Array of symbols of whitelisted fields.
|
||||
# except: Array of symbols of blacklisted fields.
|
||||
# keys: Hash of translated keys (e.g. :author => :user).
|
||||
# polymorphic: Array of symbols of polymorphic fields.
|
||||
# @return [Hash]
|
||||
#
|
||||
# @example
|
||||
# document = {
|
||||
# data: {
|
||||
# id: 1,
|
||||
# type: 'post',
|
||||
# attributes: {
|
||||
# title: 'Title 1',
|
||||
# date: '2015-12-20'
|
||||
# },
|
||||
# associations: {
|
||||
# author: {
|
||||
# data: {
|
||||
# type: 'user',
|
||||
# id: 2
|
||||
# }
|
||||
# },
|
||||
# second_author: {
|
||||
# data: nil
|
||||
# },
|
||||
# comments: {
|
||||
# data: [{
|
||||
# type: 'comment',
|
||||
# id: 3
|
||||
# },{
|
||||
# type: 'comment',
|
||||
# id: 4
|
||||
# }]
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
# }
|
||||
#
|
||||
# parse(document) #=>
|
||||
# # {
|
||||
# # title: 'Title 1',
|
||||
# # date: '2015-12-20',
|
||||
# # author_id: 2,
|
||||
# # second_author_id: nil
|
||||
# # comment_ids: [3, 4]
|
||||
# # }
|
||||
#
|
||||
# parse(document, only: [:title, :date, :author],
|
||||
# keys: { date: :published_at },
|
||||
# polymorphic: [:author]) #=>
|
||||
# # {
|
||||
# # title: 'Title 1',
|
||||
# # published_at: '2015-12-20',
|
||||
# # author_id: '2',
|
||||
# # author_type: 'people'
|
||||
# # }
|
||||
#
|
||||
def parse!(document, options = {})
|
||||
parse(document, options) do |invalid_payload, reason|
|
||||
fail InvalidDocument, "Invalid payload (#{reason}): #{invalid_payload}"
|
||||
end
|
||||
end
|
||||
|
||||
# Same as parse!, but returns an empty hash instead of raising InvalidDocument
|
||||
# on invalid payloads.
|
||||
def parse(document, options = {})
|
||||
document = document.dup.permit!.to_h if document.is_a?(ActionController::Parameters)
|
||||
|
||||
validate_payload(document) do |invalid_document, reason|
|
||||
yield invalid_document, reason if block_given?
|
||||
return {}
|
||||
end
|
||||
|
||||
primary_data = document['data']
|
||||
attributes = primary_data['attributes'] || {}
|
||||
attributes['id'] = primary_data['id'] if primary_data['id']
|
||||
relationships = primary_data['relationships'] || {}
|
||||
|
||||
filter_fields(attributes, options)
|
||||
filter_fields(relationships, options)
|
||||
|
||||
hash = {}
|
||||
hash.merge!(parse_attributes(attributes, options))
|
||||
hash.merge!(parse_relationships(relationships, options))
|
||||
|
||||
hash
|
||||
end
|
||||
|
||||
# Checks whether a payload is compliant with the JSON API spec.
|
||||
#
|
||||
# @api private
|
||||
# rubocop:disable Metrics/CyclomaticComplexity
|
||||
def validate_payload(payload)
|
||||
unless payload.is_a?(Hash)
|
||||
yield payload, 'Expected hash'
|
||||
return
|
||||
end
|
||||
|
||||
primary_data = payload['data']
|
||||
unless primary_data.is_a?(Hash)
|
||||
yield payload, { data: 'Expected hash' }
|
||||
return
|
||||
end
|
||||
|
||||
attributes = primary_data['attributes'] || {}
|
||||
unless attributes.is_a?(Hash)
|
||||
yield payload, { data: { attributes: 'Expected hash or nil' } }
|
||||
return
|
||||
end
|
||||
|
||||
relationships = primary_data['relationships'] || {}
|
||||
unless relationships.is_a?(Hash)
|
||||
yield payload, { data: { relationships: 'Expected hash or nil' } }
|
||||
return
|
||||
end
|
||||
|
||||
relationships.each do |(key, value)|
|
||||
unless value.is_a?(Hash) && value.key?('data')
|
||||
yield payload, { data: { relationships: { key => 'Expected hash with :data key' } } }
|
||||
end
|
||||
end
|
||||
end
|
||||
# rubocop:enable Metrics/CyclomaticComplexity
|
||||
|
||||
# @api private
|
||||
def filter_fields(fields, options)
|
||||
if (only = options[:only])
|
||||
fields.slice!(*Array(only).map(&:to_s))
|
||||
elsif (except = options[:except])
|
||||
fields.except!(*Array(except).map(&:to_s))
|
||||
end
|
||||
end
|
||||
|
||||
# @api private
|
||||
def field_key(field, options)
|
||||
(options[:keys] || {}).fetch(field.to_sym, field).to_sym
|
||||
end
|
||||
|
||||
# @api private
|
||||
def parse_attributes(attributes, options)
|
||||
transform_keys(attributes, options)
|
||||
.map { |(k, v)| { field_key(k, options) => v } }
|
||||
.reduce({}, :merge)
|
||||
end
|
||||
|
||||
# Given an association name, and a relationship data attribute, build a hash
|
||||
# mapping the corresponding ActiveRecord attribute to the corresponding value.
|
||||
#
|
||||
# @example
|
||||
# parse_relationship(:comments, [{ 'id' => '1', 'type' => 'comments' },
|
||||
# { 'id' => '2', 'type' => 'comments' }],
|
||||
# {})
|
||||
# # => { :comment_ids => ['1', '2'] }
|
||||
# parse_relationship(:author, { 'id' => '1', 'type' => 'users' }, {})
|
||||
# # => { :author_id => '1' }
|
||||
# parse_relationship(:author, nil, {})
|
||||
# # => { :author_id => nil }
|
||||
# @param [Symbol] assoc_name
|
||||
# @param [Hash] assoc_data
|
||||
# @param [Hash] options
|
||||
# @return [Hash{Symbol, Object}]
|
||||
#
|
||||
# @api private
|
||||
def parse_relationship(assoc_name, assoc_data, options)
|
||||
prefix_key = field_key(assoc_name, options).to_s.singularize
|
||||
hash =
|
||||
if assoc_data.is_a?(Array)
|
||||
{ "#{prefix_key}_ids".to_sym => assoc_data.map { |ri| ri['id'] } }
|
||||
else
|
||||
{ "#{prefix_key}_id".to_sym => assoc_data ? assoc_data['id'] : nil }
|
||||
end
|
||||
|
||||
polymorphic = (options[:polymorphic] || []).include?(assoc_name.to_sym)
|
||||
if polymorphic
|
||||
hash["#{prefix_key}_type".to_sym] = assoc_data.present? ? assoc_data['type'] : nil
|
||||
end
|
||||
|
||||
hash
|
||||
end
|
||||
|
||||
# @api private
|
||||
def parse_relationships(relationships, options)
|
||||
transform_keys(relationships, options)
|
||||
.map { |(k, v)| parse_relationship(k, v['data'], options) }
|
||||
.reduce({}, :merge)
|
||||
end
|
||||
|
||||
# @api private
|
||||
def transform_keys(hash, options)
|
||||
transform = options[:key_transform] || :underscore
|
||||
CaseTransform.send(transform, hash)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,96 +0,0 @@
|
||||
module ActiveModelSerializers
|
||||
module Adapter
|
||||
class JsonApi < Base
|
||||
module Error
|
||||
# rubocop:disable Style/AsciiComments
|
||||
UnknownSourceTypeError = Class.new(ArgumentError)
|
||||
|
||||
# Builds a JSON API Errors Object
|
||||
# {http://jsonapi.org/format/#errors JSON API Errors}
|
||||
#
|
||||
# @param [ActiveModel::Serializer::ErrorSerializer] error_serializer
|
||||
# @return [Array<Symbol, Array<String>>] i.e. attribute_name, [attribute_errors]
|
||||
def self.resource_errors(error_serializer, options)
|
||||
error_serializer.as_json.flat_map do |attribute_name, attribute_errors|
|
||||
attribute_name = JsonApi.send(:transform_key_casing!, attribute_name,
|
||||
options)
|
||||
attribute_error_objects(attribute_name, attribute_errors)
|
||||
end
|
||||
end
|
||||
|
||||
# definition:
|
||||
# JSON Object
|
||||
#
|
||||
# properties:
|
||||
# ☐ id : String
|
||||
# ☐ status : String
|
||||
# ☐ code : String
|
||||
# ☐ title : String
|
||||
# ☑ detail : String
|
||||
# ☐ links
|
||||
# ☐ meta
|
||||
# ☑ error_source
|
||||
#
|
||||
# description:
|
||||
# id : A unique identifier for this particular occurrence of the problem.
|
||||
# status : The HTTP status code applicable to this problem, expressed as a string value
|
||||
# code : An application-specific error code, expressed as a string value.
|
||||
# title : A short, human-readable summary of the problem. It **SHOULD NOT** change from
|
||||
# occurrence to occurrence of the problem, except for purposes of localization.
|
||||
# detail : A human-readable explanation specific to this occurrence of the problem.
|
||||
# structure:
|
||||
# {
|
||||
# title: 'SystemFailure',
|
||||
# detail: 'something went terribly wrong',
|
||||
# status: '500'
|
||||
# }.merge!(errorSource)
|
||||
def self.attribute_error_objects(attribute_name, attribute_errors)
|
||||
attribute_errors.map do |attribute_error|
|
||||
{
|
||||
source: error_source(:pointer, attribute_name),
|
||||
detail: attribute_error
|
||||
}
|
||||
end
|
||||
end
|
||||
|
||||
# errorSource
|
||||
# description:
|
||||
# oneOf
|
||||
# ☑ pointer : String
|
||||
# ☑ parameter : String
|
||||
#
|
||||
# description:
|
||||
# pointer: A JSON Pointer RFC6901 to the associated entity in the request document e.g. "/data"
|
||||
# for a primary data object, or "/data/attributes/title" for a specific attribute.
|
||||
# https://tools.ietf.org/html/rfc6901
|
||||
#
|
||||
# parameter: A string indicating which query parameter caused the error
|
||||
# structure:
|
||||
# if is_attribute?
|
||||
# {
|
||||
# pointer: '/data/attributes/red-button'
|
||||
# }
|
||||
# else
|
||||
# {
|
||||
# parameter: 'pres'
|
||||
# }
|
||||
# end
|
||||
def self.error_source(source_type, attribute_name)
|
||||
case source_type
|
||||
when :pointer
|
||||
{
|
||||
pointer: ActiveModelSerializers::JsonPointer.new(:attribute, attribute_name)
|
||||
}
|
||||
when :parameter
|
||||
{
|
||||
parameter: attribute_name
|
||||
}
|
||||
else
|
||||
fail UnknownSourceTypeError, "Unknown source type '#{source_type}' for attribute_name '#{attribute_name}'"
|
||||
end
|
||||
end
|
||||
# rubocop:enable Style/AsciiComments
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,49 +0,0 @@
|
||||
module ActiveModelSerializers
|
||||
module Adapter
|
||||
class JsonApi < Base
|
||||
# {http://jsonapi.org/format/#document-jsonapi-object Jsonapi Object}
|
||||
|
||||
# toplevel_jsonapi
|
||||
# definition:
|
||||
# JSON Object
|
||||
#
|
||||
# properties:
|
||||
# version : String
|
||||
# meta
|
||||
#
|
||||
# description:
|
||||
# An object describing the server's implementation
|
||||
# structure:
|
||||
# {
|
||||
# version: ActiveModelSerializers.config.jsonapi_version,
|
||||
# meta: ActiveModelSerializers.config.jsonapi_toplevel_meta
|
||||
# }.reject! { |_, v| v.blank? }
|
||||
# prs:
|
||||
# https://github.com/rails-api/active_model_serializers/pull/1050
|
||||
module Jsonapi
|
||||
module_function
|
||||
|
||||
def add!(hash)
|
||||
hash.merge!(object) if include_object?
|
||||
end
|
||||
|
||||
def include_object?
|
||||
ActiveModelSerializers.config.jsonapi_include_toplevel_object
|
||||
end
|
||||
|
||||
# TODO: see if we can cache this
|
||||
def object
|
||||
object = {
|
||||
jsonapi: {
|
||||
version: ActiveModelSerializers.config.jsonapi_version,
|
||||
meta: ActiveModelSerializers.config.jsonapi_toplevel_meta
|
||||
}
|
||||
}
|
||||
object[:jsonapi].reject! { |_, v| v.blank? }
|
||||
|
||||
object
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,83 +0,0 @@
|
||||
module ActiveModelSerializers
|
||||
module Adapter
|
||||
class JsonApi
|
||||
# link
|
||||
# definition:
|
||||
# oneOf
|
||||
# linkString
|
||||
# linkObject
|
||||
#
|
||||
# description:
|
||||
# A link **MUST** be represented as either: a string containing the link's URL or a link
|
||||
# object."
|
||||
# structure:
|
||||
# if href?
|
||||
# linkString
|
||||
# else
|
||||
# linkObject
|
||||
# end
|
||||
#
|
||||
# linkString
|
||||
# definition:
|
||||
# URI
|
||||
#
|
||||
# description:
|
||||
# A string containing the link's URL.
|
||||
# structure:
|
||||
# 'http://example.com/link-string'
|
||||
#
|
||||
# linkObject
|
||||
# definition:
|
||||
# JSON Object
|
||||
#
|
||||
# properties:
|
||||
# href (required) : URI
|
||||
# meta
|
||||
# structure:
|
||||
# {
|
||||
# href: 'http://example.com/link-object',
|
||||
# meta: meta,
|
||||
# }.reject! {|_,v| v.nil? }
|
||||
class Link
|
||||
include SerializationContext::UrlHelpers
|
||||
|
||||
def initialize(serializer, value)
|
||||
@_routes ||= nil # handles warning
|
||||
# actionpack-4.0.13/lib/action_dispatch/routing/route_set.rb:417: warning: instance variable @_routes not initialized
|
||||
@object = serializer.object
|
||||
@scope = serializer.scope
|
||||
# Use the return value of the block unless it is nil.
|
||||
if value.respond_to?(:call)
|
||||
@value = instance_eval(&value)
|
||||
else
|
||||
@value = value
|
||||
end
|
||||
end
|
||||
|
||||
def href(value)
|
||||
@href = value
|
||||
nil
|
||||
end
|
||||
|
||||
def meta(value)
|
||||
@meta = value
|
||||
nil
|
||||
end
|
||||
|
||||
def as_json
|
||||
return @value if @value
|
||||
|
||||
hash = {}
|
||||
hash[:href] = @href if defined?(@href)
|
||||
hash[:meta] = @meta if defined?(@meta)
|
||||
|
||||
hash.any? ? hash : nil
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :object, :scope
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
module ActiveModelSerializers
|
||||
module Adapter
|
||||
class JsonApi
|
||||
# meta
|
||||
# definition:
|
||||
# JSON Object
|
||||
#
|
||||
# description:
|
||||
# Non-standard meta-information that can not be represented as an attribute or relationship.
|
||||
# structure:
|
||||
# {
|
||||
# attitude: 'adjustable'
|
||||
# }
|
||||
class Meta
|
||||
def initialize(serializer)
|
||||
@object = serializer.object
|
||||
@scope = serializer.scope
|
||||
|
||||
# Use the return value of the block unless it is nil.
|
||||
if serializer._meta.respond_to?(:call)
|
||||
@value = instance_eval(&serializer._meta)
|
||||
else
|
||||
@value = serializer._meta
|
||||
end
|
||||
end
|
||||
|
||||
def as_json
|
||||
@value
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :object, :scope
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,69 +0,0 @@
|
||||
module ActiveModelSerializers
|
||||
module Adapter
|
||||
class JsonApi < Base
|
||||
class PaginationLinks
|
||||
MissingSerializationContextError = Class.new(KeyError)
|
||||
FIRST_PAGE = 1
|
||||
|
||||
attr_reader :collection, :context
|
||||
|
||||
def initialize(collection, adapter_options)
|
||||
@collection = collection
|
||||
@adapter_options = adapter_options
|
||||
@context = adapter_options.fetch(:serialization_context) do
|
||||
fail MissingSerializationContextError, <<-EOF.freeze
|
||||
JsonApi::PaginationLinks requires a ActiveModelSerializers::SerializationContext.
|
||||
Please pass a ':serialization_context' option or
|
||||
override CollectionSerializer#paginated? to return 'false'.
|
||||
EOF
|
||||
end
|
||||
end
|
||||
|
||||
def as_json
|
||||
per_page = collection.try(:per_page) || collection.try(:limit_value) || collection.size
|
||||
pages_from.each_with_object({}) do |(key, value), hash|
|
||||
params = query_parameters.merge(page: { number: value, size: per_page }).to_query
|
||||
|
||||
hash[key] = "#{url(adapter_options)}?#{params}"
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :adapter_options
|
||||
|
||||
private
|
||||
|
||||
def pages_from
|
||||
return {} if collection.total_pages <= FIRST_PAGE
|
||||
|
||||
{}.tap do |pages|
|
||||
pages[:self] = collection.current_page
|
||||
|
||||
unless collection.current_page == FIRST_PAGE
|
||||
pages[:first] = FIRST_PAGE
|
||||
pages[:prev] = collection.current_page - FIRST_PAGE
|
||||
end
|
||||
|
||||
unless collection.current_page == collection.total_pages
|
||||
pages[:next] = collection.current_page + FIRST_PAGE
|
||||
pages[:last] = collection.total_pages
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def url(options)
|
||||
@url ||= options.fetch(:links, {}).fetch(:self, nil) || request_url
|
||||
end
|
||||
|
||||
def request_url
|
||||
@request_url ||= context.request_url
|
||||
end
|
||||
|
||||
def query_parameters
|
||||
@query_parameters ||= context.query_parameters
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,63 +0,0 @@
|
||||
module ActiveModelSerializers
|
||||
module Adapter
|
||||
class JsonApi
|
||||
class Relationship
|
||||
# {http://jsonapi.org/format/#document-resource-object-related-resource-links Document Resource Object Related Resource Links}
|
||||
# {http://jsonapi.org/format/#document-links Document Links}
|
||||
# {http://jsonapi.org/format/#document-resource-object-linkage Document Resource Relationship Linkage}
|
||||
# {http://jsonapi.org/format/#document-meta Document Meta}
|
||||
def initialize(parent_serializer, serializable_resource_options, association)
|
||||
@parent_serializer = parent_serializer
|
||||
@association = association
|
||||
@serializable_resource_options = serializable_resource_options
|
||||
end
|
||||
|
||||
def as_json
|
||||
hash = {}
|
||||
|
||||
if association.options[:include_data]
|
||||
hash[:data] = data_for(association)
|
||||
end
|
||||
|
||||
links = links_for(association)
|
||||
hash[:links] = links if links.any?
|
||||
|
||||
meta = meta_for(association)
|
||||
hash[:meta] = meta if meta
|
||||
hash[:meta] = {} if hash.empty?
|
||||
|
||||
hash
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :parent_serializer, :serializable_resource_options, :association
|
||||
|
||||
private
|
||||
|
||||
def data_for(association)
|
||||
serializer = association.serializer
|
||||
if serializer.respond_to?(:each)
|
||||
serializer.map { |s| ResourceIdentifier.new(s, serializable_resource_options).as_json }
|
||||
elsif (virtual_value = association.options[:virtual_value])
|
||||
virtual_value
|
||||
elsif serializer && serializer.object
|
||||
ResourceIdentifier.new(serializer, serializable_resource_options).as_json
|
||||
end
|
||||
end
|
||||
|
||||
def links_for(association)
|
||||
association.links.each_with_object({}) do |(key, value), hash|
|
||||
result = Link.new(parent_serializer, value).as_json
|
||||
hash[key] = result if result
|
||||
end
|
||||
end
|
||||
|
||||
def meta_for(association)
|
||||
meta = association.meta
|
||||
meta.respond_to?(:call) ? parent_serializer.instance_eval(&meta) : meta
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,51 +0,0 @@
|
||||
module ActiveModelSerializers
|
||||
module Adapter
|
||||
class JsonApi
|
||||
class ResourceIdentifier
|
||||
def self.type_for(class_name, serializer_type = nil, transform_options = {})
|
||||
if serializer_type
|
||||
raw_type = serializer_type
|
||||
else
|
||||
inflection =
|
||||
if ActiveModelSerializers.config.jsonapi_resource_type == :singular
|
||||
:singularize
|
||||
else
|
||||
:pluralize
|
||||
end
|
||||
|
||||
raw_type = class_name.underscore
|
||||
raw_type = ActiveSupport::Inflector.public_send(inflection, raw_type)
|
||||
raw_type
|
||||
.gsub!('/'.freeze, ActiveModelSerializers.config.jsonapi_namespace_separator)
|
||||
raw_type
|
||||
end
|
||||
JsonApi.send(:transform_key_casing!, raw_type, transform_options)
|
||||
end
|
||||
|
||||
# {http://jsonapi.org/format/#document-resource-identifier-objects Resource Identifier Objects}
|
||||
def initialize(serializer, options)
|
||||
@id = id_for(serializer)
|
||||
@type = type_for(serializer, options)
|
||||
end
|
||||
|
||||
def as_json
|
||||
{ id: id, type: type }
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :id, :type
|
||||
|
||||
private
|
||||
|
||||
def type_for(serializer, transform_options)
|
||||
self.class.type_for(serializer.object.class.name, serializer._type, transform_options)
|
||||
end
|
||||
|
||||
def id_for(serializer)
|
||||
serializer.read_attribute_for_serialization(:id).to_s
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,9 +0,0 @@
|
||||
module ActiveModelSerializers
|
||||
module Adapter
|
||||
class Null < Base
|
||||
def serializable_hash(*)
|
||||
{}
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,55 +0,0 @@
|
||||
# Adapted from
|
||||
# https://github.com/rails/rails/blob/7f18ea14c8/activejob/lib/active_job/callbacks.rb
|
||||
require 'active_support/callbacks'
|
||||
|
||||
module ActiveModelSerializers
|
||||
# = ActiveModelSerializers Callbacks
|
||||
#
|
||||
# ActiveModelSerializers provides hooks during the life cycle of serialization and
|
||||
# allow you to trigger logic. Available callbacks are:
|
||||
#
|
||||
# * <tt>around_render</tt>
|
||||
#
|
||||
module Callbacks
|
||||
extend ActiveSupport::Concern
|
||||
include ActiveSupport::Callbacks
|
||||
|
||||
included do
|
||||
define_callbacks :render
|
||||
end
|
||||
|
||||
# These methods will be included into any ActiveModelSerializers object, adding
|
||||
# callbacks for +render+.
|
||||
module ClassMethods
|
||||
# Defines a callback that will get called around the render method,
|
||||
# whether it is as_json, to_json, or serializable_hash
|
||||
#
|
||||
# class ActiveModelSerializers::SerializableResource
|
||||
# include ActiveModelSerializers::Callbacks
|
||||
#
|
||||
# around_render do |args, block|
|
||||
# tag_logger do
|
||||
# notify_render do
|
||||
# block.call(args)
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# def as_json
|
||||
# run_callbacks :render do
|
||||
# adapter.as_json
|
||||
# end
|
||||
# end
|
||||
# # Note: So that we can re-use the instrumenter for as_json, to_json, and
|
||||
# # serializable_hash, we aren't using the usual format, which would be:
|
||||
# # def render(args)
|
||||
# # adapter.as_json
|
||||
# # end
|
||||
# end
|
||||
#
|
||||
def around_render(*filters, &blk)
|
||||
set_callback(:render, :around, *filters, &blk)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,54 +0,0 @@
|
||||
##
|
||||
# Provides a single method +deprecate+ to be used to declare when
|
||||
# something is going away.
|
||||
#
|
||||
# class Legacy
|
||||
# def self.klass_method
|
||||
# # ...
|
||||
# end
|
||||
#
|
||||
# def instance_method
|
||||
# # ...
|
||||
# end
|
||||
#
|
||||
# extend ActiveModelSerializers::Deprecate
|
||||
# deprecate :instance_method, "ActiveModelSerializers::NewPlace#new_method"
|
||||
#
|
||||
# class << self
|
||||
# extend ActiveModelSerializers::Deprecate
|
||||
# deprecate :klass_method, :none
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# Adapted from https://github.com/rubygems/rubygems/blob/1591331/lib/rubygems/deprecate.rb
|
||||
module ActiveModelSerializers
|
||||
module Deprecate
|
||||
##
|
||||
# Simple deprecation method that deprecates +name+ by wrapping it up
|
||||
# in a dummy method. It warns on each call to the dummy method
|
||||
# telling the user of +replacement+ (unless +replacement+ is :none) that it is planned to go away.
|
||||
|
||||
def deprecate(name, replacement)
|
||||
old = "_deprecated_#{name}"
|
||||
alias_method old, name
|
||||
class_eval do
|
||||
define_method(name) do |*args, &block|
|
||||
target = is_a?(Module) ? "#{self}." : "#{self.class}#"
|
||||
msg = ["NOTE: #{target}#{name} is deprecated",
|
||||
replacement == :none ? ' with no replacement' : "; use #{replacement} instead",
|
||||
"\n#{target}#{name} called from #{ActiveModelSerializers.location_of_caller.join(':')}"]
|
||||
warn "#{msg.join}."
|
||||
send old, *args, &block
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def delegate_and_deprecate(method, delegee)
|
||||
delegate method, to: delegee
|
||||
deprecate method, "#{delegee.name}."
|
||||
end
|
||||
|
||||
module_function :deprecate
|
||||
module_function :delegate_and_deprecate
|
||||
end
|
||||
end
|
||||
@@ -1,15 +0,0 @@
|
||||
module ActiveModelSerializers
|
||||
module Deserialization
|
||||
module_function
|
||||
|
||||
def jsonapi_parse(*args)
|
||||
Adapter::JsonApi::Deserialization.parse(*args)
|
||||
end
|
||||
|
||||
# :nocov:
|
||||
def jsonapi_parse!(*args)
|
||||
Adapter::JsonApi::Deserialization.parse!(*args)
|
||||
end
|
||||
# :nocov:
|
||||
end
|
||||
end
|
||||
@@ -1,14 +0,0 @@
|
||||
module ActiveModelSerializers
|
||||
module JsonPointer
|
||||
module_function
|
||||
|
||||
POINTERS = {
|
||||
attribute: '/data/attributes/%s'.freeze,
|
||||
primary_data: '/data%s'.freeze
|
||||
}.freeze
|
||||
|
||||
def new(pointer_type, value = nil)
|
||||
format(POINTERS[pointer_type], value)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,122 +0,0 @@
|
||||
##
|
||||
# ActiveModelSerializers::Logging
|
||||
#
|
||||
# https://github.com/rails/rails/blob/280654ef88/activejob/lib/active_job/logging.rb
|
||||
#
|
||||
module ActiveModelSerializers
|
||||
module Logging
|
||||
RENDER_EVENT = 'render.active_model_serializers'.freeze
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
include ActiveModelSerializers::Callbacks
|
||||
extend Macros
|
||||
instrument_rendering
|
||||
end
|
||||
|
||||
module ClassMethods
|
||||
def instrument_rendering
|
||||
around_render do |args, block|
|
||||
tag_logger do
|
||||
notify_render do
|
||||
block.call(args)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Macros that can be used to customize the logging of class or instance methods,
|
||||
# by extending the class or its singleton.
|
||||
#
|
||||
# Adapted from:
|
||||
# https://github.com/rubygems/rubygems/blob/cb28f5e991/lib/rubygems/deprecate.rb
|
||||
#
|
||||
# Provides a single method +notify+ to be used to declare when
|
||||
# something a method notifies, with the argument +callback_name+ of the notification callback.
|
||||
#
|
||||
# class Adapter
|
||||
# def self.klass_method
|
||||
# # ...
|
||||
# end
|
||||
#
|
||||
# def instance_method
|
||||
# # ...
|
||||
# end
|
||||
#
|
||||
# include ActiveModelSerializers::Logging::Macros
|
||||
# notify :instance_method, :render
|
||||
#
|
||||
# class << self
|
||||
# extend ActiveModelSerializers::Logging::Macros
|
||||
# notify :klass_method, :render
|
||||
# end
|
||||
# end
|
||||
module Macros
|
||||
##
|
||||
# Simple notify method that wraps up +name+
|
||||
# in a dummy method. It notifies on with the +callback_name+ notifier on
|
||||
# each call to the dummy method, telling what the current serializer and adapter
|
||||
# are being rendered.
|
||||
# Adapted from:
|
||||
# https://github.com/rubygems/rubygems/blob/cb28f5e991/lib/rubygems/deprecate.rb
|
||||
def notify(name, callback_name)
|
||||
class_eval do
|
||||
old = "_notifying_#{callback_name}_#{name}"
|
||||
alias_method old, name
|
||||
define_method name do |*args, &block|
|
||||
run_callbacks callback_name do
|
||||
send old, *args, &block
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def notify_render(*)
|
||||
event_name = RENDER_EVENT
|
||||
ActiveSupport::Notifications.instrument(event_name, notify_render_payload) do
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def notify_render_payload
|
||||
{
|
||||
serializer: serializer || ActiveModel::Serializer::Null,
|
||||
adapter: adapter || ActiveModelSerializers::Adapter::Null
|
||||
}
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def tag_logger(*tags)
|
||||
if ActiveModelSerializers.logger.respond_to?(:tagged)
|
||||
tags.unshift 'active_model_serializers'.freeze unless logger_tagged_by_active_model_serializers?
|
||||
ActiveModelSerializers.logger.tagged(*tags) { yield }
|
||||
else
|
||||
yield
|
||||
end
|
||||
end
|
||||
|
||||
def logger_tagged_by_active_model_serializers?
|
||||
ActiveModelSerializers.logger.formatter.current_tags.include?('active_model_serializers'.freeze)
|
||||
end
|
||||
|
||||
class LogSubscriber < ActiveSupport::LogSubscriber
|
||||
def render(event)
|
||||
info do
|
||||
serializer = event.payload[:serializer]
|
||||
adapter = event.payload[:adapter]
|
||||
duration = event.duration.round(2)
|
||||
"Rendered #{serializer.name} with #{adapter.class} (#{duration}ms)"
|
||||
end
|
||||
end
|
||||
|
||||
def logger
|
||||
ActiveModelSerializers.logger
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ActiveModelSerializers::Logging::LogSubscriber.attach_to :active_model_serializers
|
||||
@@ -1,80 +0,0 @@
|
||||
module ActiveModelSerializers
|
||||
module LookupChain
|
||||
# Standard appending of Serializer to the resource name.
|
||||
#
|
||||
# Example:
|
||||
# Author => AuthorSerializer
|
||||
BY_RESOURCE = lambda do |resource_class, _serializer_class, _namespace|
|
||||
serializer_from(resource_class)
|
||||
end
|
||||
|
||||
# Uses the namespace of the resource to find the serializer
|
||||
#
|
||||
# Example:
|
||||
# British::Author => British::AuthorSerializer
|
||||
BY_RESOURCE_NAMESPACE = lambda do |resource_class, _serializer_class, _namespace|
|
||||
resource_namespace = namespace_for(resource_class)
|
||||
serializer_name = serializer_from(resource_class)
|
||||
|
||||
"#{resource_namespace}::#{serializer_name}"
|
||||
end
|
||||
|
||||
# Uses the controller namespace of the resource to find the serializer
|
||||
#
|
||||
# Example:
|
||||
# Api::V3::AuthorsController => Api::V3::AuthorSerializer
|
||||
BY_NAMESPACE = lambda do |resource_class, _serializer_class, namespace|
|
||||
resource_name = resource_class_name(resource_class)
|
||||
namespace ? "#{namespace}::#{resource_name}Serializer" : nil
|
||||
end
|
||||
|
||||
# Allows for serializers to be defined in parent serializers
|
||||
# - useful if a relationship only needs a different set of attributes
|
||||
# than if it were rendered independently.
|
||||
#
|
||||
# Example:
|
||||
# class BlogSerializer < ActiveModel::Serializer
|
||||
# class AuthorSerialier < ActiveModel::Serializer
|
||||
# ...
|
||||
# end
|
||||
#
|
||||
# belongs_to :author
|
||||
# ...
|
||||
# end
|
||||
#
|
||||
# The belongs_to relationship would be rendered with
|
||||
# BlogSerializer::AuthorSerialier
|
||||
BY_PARENT_SERIALIZER = lambda do |resource_class, serializer_class, _namespace|
|
||||
return if serializer_class == ActiveModel::Serializer
|
||||
|
||||
serializer_name = serializer_from(resource_class)
|
||||
"#{serializer_class}::#{serializer_name}"
|
||||
end
|
||||
|
||||
DEFAULT = [
|
||||
BY_PARENT_SERIALIZER,
|
||||
BY_NAMESPACE,
|
||||
BY_RESOURCE_NAMESPACE,
|
||||
BY_RESOURCE
|
||||
].freeze
|
||||
|
||||
module_function
|
||||
|
||||
def namespace_for(klass)
|
||||
klass.name.deconstantize
|
||||
end
|
||||
|
||||
def resource_class_name(klass)
|
||||
klass.name.demodulize
|
||||
end
|
||||
|
||||
def serializer_from_resource_name(name)
|
||||
"#{name}Serializer"
|
||||
end
|
||||
|
||||
def serializer_from(klass)
|
||||
name = resource_class_name(klass)
|
||||
serializer_from_resource_name(name)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,129 +0,0 @@
|
||||
# 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.
|
||||
module ActiveModelSerializers
|
||||
class Model
|
||||
include ActiveModel::Serializers::JSON
|
||||
include ActiveModel::Model
|
||||
|
||||
# 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.
|
||||
#
|
||||
# @overload attribute_names
|
||||
# @return [Array<Symbol>]
|
||||
class_attribute :attribute_names, instance_writer: false, instance_reader: false
|
||||
# Initialize +attribute_names+ for all subclasses. The array is usually
|
||||
# mutated in the +attributes+ method, but can be set directly, as well.
|
||||
self.attribute_names = []
|
||||
|
||||
# Easily declare instance attributes with setters and getters for each.
|
||||
#
|
||||
# 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.
|
||||
# Accessor methods do NOT mutate the attributes. (This is a bug).
|
||||
#
|
||||
# @note For now, the Model only supports the notion of 'attributes'.
|
||||
# In the tests, there is a special Model that also supports 'associations'. This is
|
||||
# important so that we can add accessors for values that should not appear in the
|
||||
# attributes hash when modeling associations. It is not yet clear if it
|
||||
# makes sense for a PORO to have associations outside of the tests.
|
||||
#
|
||||
# @overload attributes(names)
|
||||
# @param names [Array<String, Symbol>]
|
||||
# @param name [String, Symbol]
|
||||
def self.attributes(*names)
|
||||
self.attribute_names |= names.map(&:to_sym)
|
||||
# Silence redefinition of methods warnings
|
||||
ActiveModelSerializers.silence_warnings do
|
||||
attr_accessor(*names)
|
||||
end
|
||||
end
|
||||
|
||||
# Opt-in to breaking change
|
||||
def self.derive_attributes_from_names_and_fix_accessors
|
||||
unless included_modules.include?(DeriveAttributesFromNamesAndFixAccessors)
|
||||
prepend(DeriveAttributesFromNamesAndFixAccessors)
|
||||
end
|
||||
end
|
||||
|
||||
module DeriveAttributesFromNamesAndFixAccessors
|
||||
def self.included(base)
|
||||
# NOTE that +id+ will always be in +attributes+.
|
||||
base.attributes :id
|
||||
end
|
||||
|
||||
# Override the +attributes+ method so that the hash is derived from +attribute_names+.
|
||||
#
|
||||
# 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
|
||||
self.class.attribute_names.each_with_object({}) do |attribute_name, result|
|
||||
result[attribute_name] = public_send(attribute_name).freeze
|
||||
end.with_indifferent_access.freeze
|
||||
end
|
||||
end
|
||||
|
||||
# Support for validation and other ActiveModel::Errors
|
||||
# @return [ActiveModel::Errors]
|
||||
attr_reader :errors
|
||||
|
||||
# (see #updated_at)
|
||||
attr_writer :updated_at
|
||||
|
||||
# The only way to change the attributes of an instance is to directly mutate the attributes.
|
||||
# @example
|
||||
#
|
||||
# model.attributes[:foo] = :bar
|
||||
# @return [Hash]
|
||||
attr_reader :attributes
|
||||
|
||||
# @param attributes [Hash]
|
||||
def initialize(attributes = {})
|
||||
attributes ||= {} # protect against nil
|
||||
@attributes = attributes.symbolize_keys.with_indifferent_access
|
||||
@errors = ActiveModel::Errors.new(self)
|
||||
super
|
||||
end
|
||||
|
||||
# Defaults to the downcased model name.
|
||||
# This probably isn't a good default, since it's not a unique instance identifier,
|
||||
# but that's what is currently implemented \_('-')_/.
|
||||
#
|
||||
# @note Though +id+ is defined, it will only show up
|
||||
# in +attributes+ when it is passed in to the initializer or added to +attributes+,
|
||||
# such as <tt>attributes[:id] = 5</tt>.
|
||||
# @return [String, Numeric, Symbol]
|
||||
def id
|
||||
attributes.fetch(:id) do
|
||||
defined?(@id) ? @id : self.class.model_name.name && self.class.model_name.name.downcase
|
||||
end
|
||||
end
|
||||
|
||||
# When not set, defaults to the time the file was modified.
|
||||
#
|
||||
# @note Though +updated_at+ and +updated_at=+ are defined, it will only show up
|
||||
# in +attributes+ when it is passed in to the initializer or added to +attributes+,
|
||||
# such as <tt>attributes[:updated_at] = Time.current</tt>.
|
||||
# @return [String, Numeric, Time]
|
||||
def updated_at
|
||||
attributes.fetch(:updated_at) do
|
||||
defined?(@updated_at) ? @updated_at : File.mtime(__FILE__)
|
||||
end
|
||||
end
|
||||
|
||||
# To customize model behavior, this method must be redefined. However,
|
||||
# there are other ways of setting the +cache_key+ a serializer uses.
|
||||
# @return [String]
|
||||
def cache_key
|
||||
ActiveSupport::Cache.expand_cache_key([
|
||||
self.class.model_name.name.downcase,
|
||||
"#{id}-#{updated_at.strftime('%Y%m%d%H%M%S%9N')}"
|
||||
].compact)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,48 +0,0 @@
|
||||
require 'rails/railtie'
|
||||
require 'action_controller'
|
||||
require 'action_controller/railtie'
|
||||
require 'action_controller/serialization'
|
||||
|
||||
module ActiveModelSerializers
|
||||
class Railtie < Rails::Railtie
|
||||
config.to_prepare do
|
||||
ActiveModel::Serializer.serializers_cache.clear
|
||||
end
|
||||
|
||||
initializer 'active_model_serializers.action_controller' do
|
||||
ActiveSupport.on_load(:action_controller) do
|
||||
include(::ActionController::Serialization)
|
||||
end
|
||||
end
|
||||
|
||||
initializer 'active_model_serializers.prepare_serialization_context' do
|
||||
SerializationContext.url_helpers = Rails.application.routes.url_helpers
|
||||
SerializationContext.default_url_options = Rails.application.routes.default_url_options
|
||||
end
|
||||
|
||||
# This hook is run after the action_controller railtie has set the configuration
|
||||
# based on the *environment* configuration and before any config/initializers are run
|
||||
# and also before eager_loading (if enabled).
|
||||
initializer 'active_model_serializers.set_configs', after: 'action_controller.set_configs' do
|
||||
ActiveModelSerializers.logger = Rails.configuration.action_controller.logger
|
||||
ActiveModelSerializers.config.perform_caching = Rails.configuration.action_controller.perform_caching
|
||||
# We want this hook to run after the config has been set, even if ActionController has already loaded.
|
||||
ActiveSupport.on_load(:action_controller) do
|
||||
ActiveModelSerializers.config.cache_store = ActionController::Base.cache_store
|
||||
end
|
||||
end
|
||||
|
||||
# :nocov:
|
||||
generators do |app|
|
||||
Rails::Generators.configure!(app.config.generators)
|
||||
Rails::Generators.hidden_namespaces.uniq!
|
||||
require 'generators/rails/resource_override'
|
||||
end
|
||||
# :nocov:
|
||||
|
||||
if Rails.env.test?
|
||||
ActionController::TestCase.send(:include, ActiveModelSerializers::Test::Schema)
|
||||
ActionController::TestCase.send(:include, ActiveModelSerializers::Test::Serializer)
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,78 +0,0 @@
|
||||
# Based on discussion in https://github.com/rails/rails/pull/23712#issuecomment-184977238,
|
||||
# the JSON API media type will have its own format/renderer.
|
||||
#
|
||||
# > We recommend the media type be registered on its own as jsonapi
|
||||
# when a jsonapi Renderer and deserializer (Http::Parameters::DEFAULT_PARSERS) are added.
|
||||
#
|
||||
# Usage:
|
||||
#
|
||||
# ActiveSupport.on_load(:action_controller) do
|
||||
# require 'active_model_serializers/register_jsonapi_renderer'
|
||||
# end
|
||||
#
|
||||
# And then in controllers, use `render jsonapi: model` rather than `render json: model, adapter: :json_api`.
|
||||
#
|
||||
# For example, in a controller action, we can:
|
||||
# respond_to do |format|
|
||||
# format.jsonapi { render jsonapi: model }
|
||||
# end
|
||||
#
|
||||
# or
|
||||
#
|
||||
# render jsonapi: model
|
||||
#
|
||||
# No wrapper format needed as it does not apply (i.e. no `wrap_parameters format: [jsonapi]`)
|
||||
module ActiveModelSerializers
|
||||
module Jsonapi
|
||||
MEDIA_TYPE = 'application/vnd.api+json'.freeze
|
||||
HEADERS = {
|
||||
response: { 'CONTENT_TYPE'.freeze => MEDIA_TYPE },
|
||||
request: { 'ACCEPT'.freeze => MEDIA_TYPE }
|
||||
}.freeze
|
||||
|
||||
def self.install
|
||||
# actionpack/lib/action_dispatch/http/mime_types.rb
|
||||
Mime::Type.register MEDIA_TYPE, :jsonapi
|
||||
|
||||
if Rails::VERSION::MAJOR >= 5
|
||||
ActionDispatch::Request.parameter_parsers[:jsonapi] = parser
|
||||
else
|
||||
ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime[:jsonapi]] = parser
|
||||
end
|
||||
|
||||
# ref https://github.com/rails/rails/pull/21496
|
||||
ActionController::Renderers.add :jsonapi do |json, options|
|
||||
json = serialize_jsonapi(json, options).to_json(options) unless json.is_a?(String)
|
||||
self.content_type ||= Mime[:jsonapi]
|
||||
self.response_body = json
|
||||
end
|
||||
end
|
||||
|
||||
# Proposal: should actually deserialize the JSON API params
|
||||
# to the hash format expected by `ActiveModel::Serializers::JSON`
|
||||
# actionpack/lib/action_dispatch/http/parameters.rb
|
||||
def self.parser
|
||||
lambda do |body|
|
||||
data = JSON.parse(body)
|
||||
data = { _json: data } unless data.is_a?(Hash)
|
||||
data.with_indifferent_access
|
||||
end
|
||||
end
|
||||
|
||||
module ControllerSupport
|
||||
def serialize_jsonapi(json, options)
|
||||
options[:adapter] = :json_api
|
||||
options.fetch(:serialization_context) do
|
||||
options[:serialization_context] = ActiveModelSerializers::SerializationContext.new(request)
|
||||
end
|
||||
get_serializer(json, options)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
ActiveModelSerializers::Jsonapi.install
|
||||
|
||||
ActiveSupport.on_load(:action_controller) do
|
||||
include ActiveModelSerializers::Jsonapi::ControllerSupport
|
||||
end
|
||||
@@ -1,82 +0,0 @@
|
||||
require 'set'
|
||||
|
||||
module ActiveModelSerializers
|
||||
class SerializableResource
|
||||
ADAPTER_OPTION_KEYS = Set.new([:include, :fields, :adapter, :meta, :meta_key, :links, :serialization_context, :key_transform])
|
||||
include ActiveModelSerializers::Logging
|
||||
|
||||
delegate :serializable_hash, :as_json, :to_json, to: :adapter
|
||||
notify :serializable_hash, :render
|
||||
notify :as_json, :render
|
||||
notify :to_json, :render
|
||||
|
||||
# Primary interface to composing a resource with a serializer and adapter.
|
||||
# @return the serializable_resource, ready for #as_json/#to_json/#serializable_hash.
|
||||
def initialize(resource, options = {})
|
||||
@resource = resource
|
||||
@adapter_opts, @serializer_opts =
|
||||
options.partition { |k, _| ADAPTER_OPTION_KEYS.include? k }.map { |h| Hash[h] }
|
||||
end
|
||||
|
||||
def serialization_scope=(scope)
|
||||
serializer_opts[:scope] = scope
|
||||
end
|
||||
|
||||
def serialization_scope
|
||||
serializer_opts[:scope]
|
||||
end
|
||||
|
||||
def serialization_scope_name=(scope_name)
|
||||
serializer_opts[:scope_name] = scope_name
|
||||
end
|
||||
|
||||
# NOTE: if no adapter is available, returns the resource itself. (i.e. adapter is a no-op)
|
||||
def adapter
|
||||
@adapter ||= find_adapter
|
||||
end
|
||||
alias adapter_instance adapter
|
||||
|
||||
def find_adapter
|
||||
return resource unless serializer?
|
||||
adapter = catch :no_serializer do
|
||||
ActiveModelSerializers::Adapter.create(serializer_instance, adapter_opts)
|
||||
end
|
||||
adapter || resource
|
||||
end
|
||||
|
||||
def serializer_instance
|
||||
@serializer_instance ||= serializer.new(resource, serializer_opts)
|
||||
end
|
||||
|
||||
# Get serializer either explicitly :serializer or implicitly from resource
|
||||
# Remove :serializer key from serializer_opts
|
||||
# Remove :each_serializer if present and set as :serializer key
|
||||
def serializer
|
||||
@serializer ||=
|
||||
begin
|
||||
@serializer = serializer_opts.delete(:serializer)
|
||||
@serializer ||= ActiveModel::Serializer.serializer_for(resource, serializer_opts)
|
||||
|
||||
if serializer_opts.key?(:each_serializer)
|
||||
serializer_opts[:serializer] = serializer_opts.delete(:each_serializer)
|
||||
end
|
||||
@serializer
|
||||
end
|
||||
end
|
||||
alias serializer_class serializer
|
||||
|
||||
# True when no explicit adapter given, or explicit appear is truthy (non-nil)
|
||||
# False when explicit adapter is falsy (nil or false)
|
||||
def use_adapter?
|
||||
!(adapter_opts.key?(:adapter) && !adapter_opts[:adapter])
|
||||
end
|
||||
|
||||
def serializer?
|
||||
use_adapter? && !serializer.nil?
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :resource, :adapter_opts, :serializer_opts
|
||||
end
|
||||
end
|
||||
@@ -1,39 +0,0 @@
|
||||
require 'active_support/core_ext/array/extract_options'
|
||||
module ActiveModelSerializers
|
||||
class SerializationContext
|
||||
class << self
|
||||
attr_writer :url_helpers, :default_url_options
|
||||
def url_helpers
|
||||
@url_helpers ||= Module.new
|
||||
end
|
||||
|
||||
def default_url_options
|
||||
@default_url_options ||= {}
|
||||
end
|
||||
end
|
||||
module UrlHelpers
|
||||
def self.included(base)
|
||||
base.send(:include, SerializationContext.url_helpers)
|
||||
end
|
||||
|
||||
def default_url_options
|
||||
SerializationContext.default_url_options
|
||||
end
|
||||
end
|
||||
|
||||
attr_reader :request_url, :query_parameters, :key_transform
|
||||
|
||||
def initialize(*args)
|
||||
options = args.extract_options!
|
||||
if args.size == 1
|
||||
request = args.pop
|
||||
options[:request_url] = request.original_url[/\A[^?]+/]
|
||||
options[:query_parameters] = request.query_parameters
|
||||
end
|
||||
@request_url = options.delete(:request_url)
|
||||
@query_parameters = options.delete(:query_parameters)
|
||||
@url_helpers = options.delete(:url_helpers) || self.class.url_helpers
|
||||
@default_url_options = options.delete(:default_url_options) || self.class.default_url_options
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,7 +0,0 @@
|
||||
module ActiveModelSerializers
|
||||
module Test
|
||||
extend ActiveSupport::Autoload
|
||||
autoload :Serializer
|
||||
autoload :Schema
|
||||
end
|
||||
end
|
||||
@@ -1,138 +0,0 @@
|
||||
module ActiveModelSerializers
|
||||
module Test
|
||||
module Schema
|
||||
# A Minitest Assertion that test the response is valid against a schema.
|
||||
# @param schema_path [String] a custom schema path
|
||||
# @param message [String] a custom error message
|
||||
# @return [Boolean] true when the response is valid
|
||||
# @return [Minitest::Assertion] when the response is invalid
|
||||
# @example
|
||||
# get :index
|
||||
# assert_response_schema
|
||||
def assert_response_schema(schema_path = nil, message = nil)
|
||||
matcher = AssertResponseSchema.new(schema_path, request, response, message)
|
||||
assert(matcher.call, matcher.message)
|
||||
end
|
||||
|
||||
def assert_request_schema(schema_path = nil, message = nil)
|
||||
matcher = AssertRequestSchema.new(schema_path, request, response, message)
|
||||
assert(matcher.call, matcher.message)
|
||||
end
|
||||
|
||||
# May be renamed
|
||||
def assert_request_response_schema(schema_path = nil, message = nil)
|
||||
assert_request_schema(schema_path, message)
|
||||
assert_response_schema(schema_path, message)
|
||||
end
|
||||
|
||||
def assert_schema(payload, schema_path = nil, message = nil)
|
||||
matcher = AssertSchema.new(schema_path, request, response, message, payload)
|
||||
assert(matcher.call, matcher.message)
|
||||
end
|
||||
|
||||
MissingSchema = Class.new(Minitest::Assertion)
|
||||
InvalidSchemaError = Class.new(Minitest::Assertion)
|
||||
|
||||
class AssertSchema
|
||||
attr_reader :schema_path, :request, :response, :message, :payload
|
||||
|
||||
# Interface may change.
|
||||
def initialize(schema_path, request, response, message, payload = nil)
|
||||
require_json_schema!
|
||||
@request = request
|
||||
@response = response
|
||||
@payload = payload
|
||||
@schema_path = schema_path || schema_path_default
|
||||
@message = message
|
||||
@document_store = JsonSchema::DocumentStore.new
|
||||
add_schema_to_document_store
|
||||
end
|
||||
|
||||
def call
|
||||
json_schema.expand_references!(store: document_store)
|
||||
status, errors = json_schema.validate(response_body)
|
||||
@message = [message, errors.map(&:to_s).to_sentence].compact.join(': ')
|
||||
status
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :document_store
|
||||
|
||||
def controller_path
|
||||
request.filtered_parameters.with_indifferent_access[:controller]
|
||||
end
|
||||
|
||||
def action
|
||||
request.filtered_parameters.with_indifferent_access[:action]
|
||||
end
|
||||
|
||||
def schema_directory
|
||||
ActiveModelSerializers.config.schema_path
|
||||
end
|
||||
|
||||
def schema_full_path
|
||||
"#{schema_directory}/#{schema_path}"
|
||||
end
|
||||
|
||||
def schema_path_default
|
||||
"#{controller_path}/#{action}.json"
|
||||
end
|
||||
|
||||
def schema_data
|
||||
load_json_file(schema_full_path)
|
||||
end
|
||||
|
||||
def response_body
|
||||
load_json(response.body)
|
||||
end
|
||||
|
||||
def request_params
|
||||
request.env['action_dispatch.request.request_parameters']
|
||||
end
|
||||
|
||||
def json_schema
|
||||
@json_schema ||= JsonSchema.parse!(schema_data)
|
||||
end
|
||||
|
||||
def add_schema_to_document_store
|
||||
Dir.glob("#{schema_directory}/**/*.json").each do |path|
|
||||
schema_data = load_json_file(path)
|
||||
extra_schema = JsonSchema.parse!(schema_data)
|
||||
document_store.add_schema(extra_schema)
|
||||
end
|
||||
end
|
||||
|
||||
def load_json(json)
|
||||
JSON.parse(json)
|
||||
rescue JSON::ParserError => ex
|
||||
raise InvalidSchemaError, ex.message
|
||||
end
|
||||
|
||||
def load_json_file(path)
|
||||
load_json(File.read(path))
|
||||
rescue Errno::ENOENT
|
||||
raise MissingSchema, "No Schema file at #{schema_full_path}"
|
||||
end
|
||||
|
||||
def require_json_schema!
|
||||
require 'json_schema'
|
||||
rescue LoadError
|
||||
raise LoadError, "You don't have json_schema installed in your application. Please add it to your Gemfile and run bundle install"
|
||||
end
|
||||
end
|
||||
class AssertResponseSchema < AssertSchema
|
||||
def initialize(*)
|
||||
super
|
||||
@payload = response_body
|
||||
end
|
||||
end
|
||||
class AssertRequestSchema < AssertSchema
|
||||
def initialize(*)
|
||||
super
|
||||
@payload = request_params
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,125 +0,0 @@
|
||||
require 'set'
|
||||
module ActiveModelSerializers
|
||||
module Test
|
||||
module Serializer
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
setup :setup_serialization_subscriptions
|
||||
teardown :teardown_serialization_subscriptions
|
||||
end
|
||||
|
||||
# Asserts that the request was rendered with the appropriate serializers.
|
||||
#
|
||||
# # assert that the "PostSerializer" serializer was rendered
|
||||
# assert_serializer "PostSerializer"
|
||||
#
|
||||
# # return a custom error message
|
||||
# assert_serializer "PostSerializer", "PostSerializer not rendered"
|
||||
#
|
||||
# # assert that the instance of PostSerializer was rendered
|
||||
# assert_serializer PostSerializer
|
||||
#
|
||||
# # assert that the "PostSerializer" serializer was rendered
|
||||
# assert_serializer :post_serializer
|
||||
#
|
||||
# # assert that the rendered serializer starts with "Post"
|
||||
# assert_serializer %r{\APost.+\Z}
|
||||
#
|
||||
# # assert that no serializer was rendered
|
||||
# assert_serializer nil
|
||||
#
|
||||
def assert_serializer(expectation, message = nil)
|
||||
@assert_serializer.expectation = expectation
|
||||
@assert_serializer.message = message
|
||||
@assert_serializer.response = response
|
||||
assert(@assert_serializer.matches?, @assert_serializer.message)
|
||||
end
|
||||
|
||||
class AssertSerializer
|
||||
attr_reader :serializers, :message
|
||||
attr_accessor :response, :expectation
|
||||
|
||||
def initialize
|
||||
@serializers = Set.new
|
||||
@_subscribers = []
|
||||
end
|
||||
|
||||
def message=(message)
|
||||
@message = message || "expecting <#{expectation.inspect}> but rendering with <#{serializers.to_a}>"
|
||||
end
|
||||
|
||||
def matches?
|
||||
# Force body to be read in case the template is being streamed.
|
||||
response.body
|
||||
|
||||
case expectation
|
||||
when a_serializer? then matches_class?
|
||||
when Symbol then matches_symbol?
|
||||
when String then matches_string?
|
||||
when Regexp then matches_regexp?
|
||||
when NilClass then matches_nil?
|
||||
else fail ArgumentError, 'assert_serializer only accepts a String, Symbol, Regexp, ActiveModel::Serializer, or nil'
|
||||
end
|
||||
end
|
||||
|
||||
def subscribe
|
||||
@_subscribers << ActiveSupport::Notifications.subscribe(event_name) do |_name, _start, _finish, _id, payload|
|
||||
serializer = payload[:serializer].name
|
||||
serializers << serializer
|
||||
end
|
||||
end
|
||||
|
||||
def unsubscribe
|
||||
@_subscribers.each do |subscriber|
|
||||
ActiveSupport::Notifications.unsubscribe(subscriber)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def matches_class?
|
||||
serializers.include?(expectation.name)
|
||||
end
|
||||
|
||||
def matches_symbol?
|
||||
camelize_expectation = expectation.to_s.camelize
|
||||
serializers.include?(camelize_expectation)
|
||||
end
|
||||
|
||||
def matches_string?
|
||||
!expectation.empty? && serializers.include?(expectation)
|
||||
end
|
||||
|
||||
def matches_regexp?
|
||||
serializers.any? do |serializer|
|
||||
serializer.match(expectation)
|
||||
end
|
||||
end
|
||||
|
||||
def matches_nil?
|
||||
serializers.empty?
|
||||
end
|
||||
|
||||
def a_serializer?
|
||||
->(exp) { exp.is_a?(Class) && exp < ActiveModel::Serializer }
|
||||
end
|
||||
|
||||
def event_name
|
||||
::ActiveModelSerializers::Logging::RENDER_EVENT
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def setup_serialization_subscriptions
|
||||
@assert_serializer = AssertSerializer.new
|
||||
@assert_serializer.subscribe
|
||||
end
|
||||
|
||||
def teardown_serialization_subscriptions
|
||||
@assert_serializer.unsubscribe
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
2
lib/ams.rb
Normal file
2
lib/ams.rb
Normal file
@@ -0,0 +1,2 @@
|
||||
# frozen_string_literal: true
|
||||
require 'ams/version'
|
||||
4
lib/ams/version.rb
Normal file
4
lib/ams/version.rb
Normal file
@@ -0,0 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
module AMS
|
||||
VERSION = '0.99.0'
|
||||
end
|
||||
@@ -1,6 +0,0 @@
|
||||
Description:
|
||||
Generates a serializer for the given resource.
|
||||
|
||||
Example:
|
||||
`rails generate serializer Account name created_at`
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
require 'rails/generators'
|
||||
require 'rails/generators/rails/resource/resource_generator'
|
||||
|
||||
module Rails
|
||||
module Generators
|
||||
class ResourceGenerator
|
||||
hook_for :serializer, default: true, type: :boolean
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,36 +0,0 @@
|
||||
module Rails
|
||||
module Generators
|
||||
class SerializerGenerator < NamedBase
|
||||
source_root File.expand_path('../templates', __FILE__)
|
||||
check_class_collision suffix: 'Serializer'
|
||||
|
||||
argument :attributes, type: :array, default: [], banner: 'field:type field:type'
|
||||
|
||||
class_option :parent, type: :string, desc: 'The parent class for the generated serializer'
|
||||
|
||||
def create_serializer_file
|
||||
template 'serializer.rb.erb', File.join('app/serializers', class_path, "#{file_name}_serializer.rb")
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def attributes_names
|
||||
[:id] + attributes.reject(&:reference?).map! { |a| a.name.to_sym }
|
||||
end
|
||||
|
||||
def association_names
|
||||
attributes.select(&:reference?).map! { |a| a.name.to_sym }
|
||||
end
|
||||
|
||||
def parent_class_name
|
||||
if options[:parent]
|
||||
options[:parent]
|
||||
elsif 'ApplicationSerializer'.safe_constantize
|
||||
'ApplicationSerializer'
|
||||
else
|
||||
'ActiveModel::Serializer'
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,8 +0,0 @@
|
||||
<% module_namespacing do -%>
|
||||
class <%= class_name %>Serializer < <%= parent_class_name %>
|
||||
attributes <%= attributes_names.map(&:inspect).join(", ") %>
|
||||
<% association_names.each do |attribute| -%>
|
||||
has_one :<%= attribute %>
|
||||
<% end -%>
|
||||
end
|
||||
<% end -%>
|
||||
@@ -1,16 +0,0 @@
|
||||
# To add Grape support, require 'grape/active_model_serializers' in the base of your Grape endpoints
|
||||
# Then add 'include Grape::ActiveModelSerializers' to enable the formatter and helpers
|
||||
require 'active_model_serializers'
|
||||
require 'grape/formatters/active_model_serializers'
|
||||
require 'grape/helpers/active_model_serializers'
|
||||
|
||||
module Grape
|
||||
module ActiveModelSerializers
|
||||
extend ActiveSupport::Concern
|
||||
|
||||
included do
|
||||
formatter :json, Grape::Formatters::ActiveModelSerializers
|
||||
helpers Grape::Helpers::ActiveModelSerializers
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,32 +0,0 @@
|
||||
# A Grape response formatter that can be used as 'formatter :json, Grape::Formatters::ActiveModelSerializers'
|
||||
#
|
||||
# Serializer options can be passed as a hash from your Grape endpoint using env[:active_model_serializer_options],
|
||||
# or better yet user the render helper in Grape::Helpers::ActiveModelSerializers
|
||||
|
||||
require 'active_model_serializers/serialization_context'
|
||||
|
||||
module Grape
|
||||
module Formatters
|
||||
module ActiveModelSerializers
|
||||
def self.call(resource, env)
|
||||
serializer_options = build_serializer_options(env)
|
||||
::ActiveModelSerializers::SerializableResource.new(resource, serializer_options).to_json
|
||||
end
|
||||
|
||||
def self.build_serializer_options(env)
|
||||
ams_options = env[:active_model_serializer_options] || {}
|
||||
|
||||
# Add serialization context
|
||||
ams_options.fetch(:serialization_context) do
|
||||
request = env['grape.request']
|
||||
ams_options[:serialization_context] = ::ActiveModelSerializers::SerializationContext.new(
|
||||
request_url: request.url[/\A[^?]+/],
|
||||
query_parameters: request.params
|
||||
)
|
||||
end
|
||||
|
||||
ams_options
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,17 +0,0 @@
|
||||
# Helpers can be included in your Grape endpoint as: helpers Grape::Helpers::ActiveModelSerializers
|
||||
|
||||
module Grape
|
||||
module Helpers
|
||||
module ActiveModelSerializers
|
||||
# A convenience method for passing ActiveModelSerializers serializer options
|
||||
#
|
||||
# Example: To include relationships in the response: render(post, include: ['comments'])
|
||||
#
|
||||
# Example: To include pagination meta data: render(posts, meta: { page: posts.page, total_pages: posts.total_pages })
|
||||
def render(resource, active_model_serializer_options = {})
|
||||
env[:active_model_serializer_options] = active_model_serializer_options
|
||||
resource
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
26
lib/tasks/doc.rake
Normal file
26
lib/tasks/doc.rake
Normal file
@@ -0,0 +1,26 @@
|
||||
# frozen_string_literal: true
|
||||
require 'yard'
|
||||
|
||||
namespace :doc do
|
||||
desc 'start a gem server'
|
||||
task :server do
|
||||
sh 'bundle exec yard server --gems'
|
||||
end
|
||||
|
||||
desc 'use Graphviz to generate dot graph'
|
||||
task :graph do
|
||||
output_file = 'doc/erd.dot'
|
||||
sh "bundle exec yard graph --protected --full --dependencies > #{output_file}"
|
||||
puts 'open doc/erd.dot if you have graphviz installed'
|
||||
end
|
||||
|
||||
YARD::Rake::YardocTask.new(:stats) do |t|
|
||||
t.stats_options = ['--list-undoc']
|
||||
end
|
||||
|
||||
DOC_PATH = File.join('doc')
|
||||
YARD::Rake::YardocTask.new(:pages) do |t|
|
||||
t.options = ['-o', DOC_PATH]
|
||||
end
|
||||
end
|
||||
task doc: ['doc:pages']
|
||||
@@ -1,3 +1,4 @@
|
||||
# frozen_string_literal: true
|
||||
begin
|
||||
require 'rubocop'
|
||||
require 'rubocop/rake_task'
|
||||
|
||||
11
lib/tasks/test.rake
Normal file
11
lib/tasks/test.rake
Normal file
@@ -0,0 +1,11 @@
|
||||
# frozen_string_literal: true
|
||||
require 'rake/testtask'
|
||||
|
||||
Rake::TestTask.new(:test) do |t|
|
||||
t.libs << 'lib'
|
||||
t.libs << 'test'
|
||||
t.pattern = 'test/**/*_test.rb'
|
||||
t.ruby_opts = ['-r./test/test_helper.rb']
|
||||
t.ruby_opts << ' -w' unless ENV['NO_WARN'] == 'true'
|
||||
t.verbose = true
|
||||
end
|
||||
Reference in New Issue
Block a user