Альфа-версия Rails 5 / Ruby 2.2.3 / active_model_serializers (0.10.0.rc3) (далее AMS)
GIT
remote: https://github.com/rails/rails.git
revision: 5217db2a7acb80b18475709018088535bdec6d30
GEM
remote: https://rubygems.org/
specs:
active_model_serializers (0.10.0.rc3)
У меня уже есть работающее приложение только для API, использующее Rabl-Rails для генерации ответов JSON.
Я работаю над тем, чтобы сделать его готовым к работе с Rails 5, и в рамках этого оцениваю встроенные функции API Rails 5, особенно возможность повторного использования и гибкость, которые может предоставить ActiveModel::Serializers.
Я создал несколько сериализаторов
- app
- serializers
- client
- base_serializer.rb
- device
- base_serializer.rb
- provider
- base_serializer.rb
Ответы JSON в приложении создаются составным образом с повторным использованием любых существующих шаблонов, а base_serializer содержит основные данные о ресурсе, которые можно сериализовать.
Ниже показаны 3 сериализатора, которые я изначально создал:
Для устройства модели ActiveRecord
class Device::BaseSerializer < ActiveModel::Serializer
attributes :id, :name
belongs_to :client, serializer: Client::BaseSerializer
def client
unless exclude_client?
object.client
end
end
private
def device_opts?
options.key?(:device) # options inherited from by ActiveModel::Serializer
end
def device_opts
options[:device]
end
def exclude_client?
device_opts? && device_opts.key?(:exclude_client) # options inherited from by ActiveModel::Serializer
end
end
Для клиента модели ActiveRecord
class Client::BaseSerializer < ActiveModel::Serializer
attributes :id, :name, :time_zone
belongs_to :provider, serializer: Provider::BaseSerializer
def provider
unless exclude_provider?
object.provider
end
end
private
def client_opts?
options.key?(:client) # options inherited from by ActiveModel::Serializer
end
def client_opts
options[:client]
end
def exclude_provider?
client_opts? && client_opts.key?(:exclude_provider)
end
end
Для поставщика модели ActiveRecord
class Provider::BaseSerializer < ActiveModel::Serializer
attributes :id, :name
end
Модель поставщика
class Provider < ActiveRecord::Base
has_many :clients, dependent: :destroy
end
Модель клиента
class Client < ActiveRecord::Base
belongs_to :provider
has_many :devices
end
Модель устройства
class Device < ActiveRecord::Base
belongs_to :client
end
Проблема
При сериализации объекта Device его узел client не содержит узел provider клиента, как определено параметром belongs_to :provider, serializer: Provider::BaseSerializer
в Client::BaseSerializer.
device = Device.find(1)
options = { serializer: Device::BaseSerializer }
serializable_resource = ActiveModel::SerializableResource.new(device, options)
puts JSON.pretty_generate(serializable_resource.as_json)
{
"id": 1,
"name": "Test Device",
"client": {
"id": 2,
"name": "Test Client",
"time_zone": "Eastern Time (US & Canada)"
}
}
Однако при сериализации объекта Client он содержит узел provider:
client = Client.find(2)
options = { serializer: Client::BaseSerializer }
serializable_resource = ActiveModel::SerializableResource.new(client, options)
puts JSON.pretty_generate(serializable_resource.as_json)
{
"id": 2,
"name": "Test Client",
"time_zone": "Eastern Time (US & Canada)",
"provider": {
"id": 1,
"name": "Test Provider"
}
}
Как видно выше, когда мы генерируем json устройства в его свойстве «client», отношение поставщика клиента не создается. Однако, когда мы генерируем json клиента, он содержит свойство «поставщик». Передача любой опции включения, например следующей
options = { serializer: Client::BaseSerializer, include: "client.provider.**" }
как упоминалось здесь, не имеет никакого эффекта.
Я копался в исходном коде AMS и обнаружил, что вариант include
рассматривается только в том случае, если адаптер JSON API
. Однако адаптер по умолчанию — base.config.adapter = :flatten_json
.
Как видно из реализации адаптера и метода ActiveModel::Serializer#attributes(options = {})
(показан ниже), при сериализации учитываются только следующие данные:
- Атрибуты объекта
- Отношения объектов и их атрибуты. Собственные отношения отношения не учитываются.
ActiveModel::Serializer::Adapter::FlattenJson ‹ ActiveModel::Serializer::Adapter::Json
def serializable_hash(options = nil)
options ||= {}
if serializer.respond_to?(:each)
result = serializer.map { |s| FlattenJson.new(s).serializable_hash(options) }
else
hash = {}
core = cache_check(serializer) do
serializer.attributes(options)
end
serializer.associations.each do |association|
serializer = association.serializer
opts = association.options
if serializer.respond_to?(:each)
array_serializer = serializer
hash[association.key] = array_serializer.map do |item|
cache_check(item) do
item.attributes(opts)
end
end
else
hash[association.key] =
if serializer && serializer.object
cache_check(serializer) do
# As can be seen here ASSOCIATION's serializer's attributes only gets serialize and not its own relations.
serializer.attributes(options)
end
elsif opts[:virtual_value]
opts[:virtual_value]
end
end
end
result = core.merge hash
end
ActiveModel::Serializer
def attributes(options = {})
attributes =
if options[:fields]
self.class._attributes & options[:fields]
else
self.class._attributes.dup # <<<<<<<<<< here
end
attributes.each_with_object({}) do |name, hash|
unless self.class._fragmented
hash[name] = send(name)
else
hash[name] = self.class._fragmented.public_send(name)
end
end
end
Поскольку мои структуры JSON являются пользовательскими и предопределенными, я не могу переключиться на адаптер JSON API. Это оставляет возможность использовать только атрибуты, такие как
class Client::BaseSerializer < ActiveModel::Serializer
attributes :id, :name, :time_zone
attributes :provider
def provider
unless exclude_provider?
object.provider
end
end
end
Но таким образом я не могу использовать собственный сериализатор для атрибута :provider.
Вопросы:
Есть ли способ обойти упомянутую выше проблему и добиться желаемых результатов?
Есть ли возможность игнорирования включения атрибута в сериализованный хэш? Например, используя Rabl-Rails в моем шаблоне JSON, я могу сделать следующее:
node(:client, if: ->(device_decorator) { !device_decorator.exclude_client? } ) do |device_decorator| partial('../clients/base', object: device_decorator.client_decorator) end
При этом, если DeviceDecorator#exclude_client?
возвращает false, узел :client не генерируется в JSON.
Спасибо.