ActiveModelSerializers (0.10.0.rc3) отношение отношения объекта не создается с адаптером FlattenJson по умолчанию

Альфа-версия 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 = {}) (показан ниже), при сериализации учитываются только следующие данные:

  1. Атрибуты объекта
  2. Отношения объектов и их атрибуты. Собственные отношения отношения не учитываются.

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.

Вопросы:

  1. Есть ли способ обойти упомянутую выше проблему и добиться желаемых результатов?

  2. Есть ли возможность игнорирования включения атрибута в сериализованный хэш? Например, используя 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.

Спасибо.


person Jignesh Gohel    schedule 30.11.2015    source источник