ActiveModelSerializers (0.10.0.rc3) an object'

2019-07-13 22:12发布

Rails 5 Alpha version / Ruby 2.2.3 / active_model_serializers (0.10.0.rc3) (henceforth referred as AMS)

GIT
  remote: https://github.com/rails/rails.git
  revision: 5217db2a7acb80b18475709018088535bdec6d30

GEM
  remote: https://rubygems.org/
  specs:
    active_model_serializers (0.10.0.rc3)

I already have a working API-only app utilizing Rabl-Rails to generate the JSON responses.

I am working on making it ready to work with Rails 5 and as part of this evaluating Rails 5 inbuilt API features esp the reusability and flexibility ActiveModel::Serializers can provide.

I created few serializers

- app
  - serializers 
    - client
      - base_serializer.rb
    - device
      - base_serializer.rb
    - provider
      - base_serializer.rb

The JSON responses in the app is created in composite manner reusing any existing templates and a base_serializer contains basic data about a resource which can be serialized.

Below shown are the 3 serializers I have initially created:

For ActiveRecord model Device

  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

For ActiveRecord model Client

  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

For ActiveRecord model Provider

  class Provider::BaseSerializer < ActiveModel::Serializer
    attributes :id, :name
  end

Provider model

class Provider < ActiveRecord::Base
  has_many :clients, dependent: :destroy
end

Client model

class Client < ActiveRecord::Base
  belongs_to :provider
  has_many :devices
end

Device model

class Device < ActiveRecord::Base
  belongs_to :client
end

Problem

When serializing a Device object its client node doesn't contain the client's provider node as defined by belongs_to :provider, serializer: Provider::BaseSerializer in 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)"
  }
}

However when serializing a Client object it contains the provider node:

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"
  }
}

As can be seen above when we generate a Device's json in its "client" property, client's provider relation is not getting generated. However when we generate a Client's json it contains "provider" property. Passing any include option like following

options = { serializer: Client::BaseSerializer, include: "client.provider.**" }

as mentioned here doesn't have any effect.

I dig into the AMS source code and found that include option is only considered if the adapter is JSON API. However, the default adapter is base.config.adapter = :flatten_json.

As can be seen in the adapter's implementation and ActiveModel::Serializer#attributes(options = {}) method (shown below) only following data is taken into account during serialization:

  1. Object's attributes
  2. Object's relationships and their attributes. Relationship's own relations are not taken into account.

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

As my JSON structures are custom and pre-defined, I cannot switch to JSON API adapter. That leaves the option to use only attributes like

  class Client::BaseSerializer < ActiveModel::Serializer
    attributes :id, :name, :time_zone

    attributes :provider

    def provider
      unless exclude_provider?
        object.provider
      end
    end
  end

But that way I do not get flexibility to use a custom serializer for :provider attribute.

Questions:

  1. Is there any way to get around the problem mentioned above and achieve the desired results?

  2. Is there any provision to ignore an attribute from getting included in the serialized hash? For e.g. using Rabl-Rails in my JSON template I can do following:

    node(:client, if: ->(device_decorator) {  !device_decorator.exclude_client? } ) do |device_decorator|
      partial('../clients/base', object: device_decorator.client_decorator)
    end
    

With that in place if DeviceDecorator#exclude_client? returns false, :client node doesn't get generated in the JSON.

Thanks.

0条回答
登录 后发表回答