Clean slate

This commit is contained in:
Benjamin Fleischer
2017-04-13 21:46:26 -05:00
parent 04125a06b0
commit a791070a29
218 changed files with 250 additions and 20176 deletions

View File

@@ -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

View File

@@ -1,11 +0,0 @@
require 'set'
module ActiveModel
class SerializableResource
class << self
extend ActiveModelSerializers::Deprecate
delegate_and_deprecate :new, ActiveModelSerializers::SerializableResource
end
end
end

View File

@@ -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

View File

@@ -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'

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

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

View File

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

View File

@@ -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

View File

@@ -1,17 +0,0 @@
module ActiveModel
class Serializer
class Null < Serializer
def attributes(*)
{}
end
def associations(*)
{}
end
def serializable_hash(*)
{}
end
end
end
end

View File

@@ -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

View File

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

View File

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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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` members 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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,9 +0,0 @@
module ActiveModelSerializers
module Adapter
class Null < Base
def serializable_hash(*)
{}
end
end
end
end

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -1,7 +0,0 @@
module ActiveModelSerializers
module Test
extend ActiveSupport::Autoload
autoload :Serializer
autoload :Schema
end
end

View File

@@ -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

View File

@@ -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
View File

@@ -0,0 +1,2 @@
# frozen_string_literal: true
require 'ams/version'

4
lib/ams/version.rb Normal file
View File

@@ -0,0 +1,4 @@
# frozen_string_literal: true
module AMS
VERSION = '0.99.0'
end

View File

@@ -1,6 +0,0 @@
Description:
Generates a serializer for the given resource.
Example:
`rails generate serializer Account name created_at`

View File

@@ -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

View File

@@ -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

View File

@@ -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 -%>

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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']

View File

@@ -1,3 +1,4 @@
# frozen_string_literal: true
begin
require 'rubocop'
require 'rubocop/rake_task'

11
lib/tasks/test.rake Normal file
View 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