Skip to content

Add optional fields support for associations #1284

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed

Add optional fields support for associations #1284

wants to merge 1 commit into from

Conversation

vasilakisfil
Copy link
Contributor

I am really fond of JSONAPI but there are people that they still need JSON (mostly) and Attributes adapters :) Plus, we should opt for best migration from 0.8.x and 0.9x adapters.

There is a minor bug in the fields option in master branch. If you have:

render json: @videos.limit(1), each_serializer: VideoSerializer,  fields: [:id],

This will apply the fields to all associations too. We don't want that. What we want is:

If there is a fields param, apply it to the main resource. If there are associations, serialize the associations without removing any attributes. For instance:

render json: @videos.limit(1), each_serializer: VideoSerializer,  fields: [:id]

Then this should apply the fields: [:id] only in the VideoSerializer and not in any association.

If there are associations in the fields array (just like in StrongParams) and these also exist in the main resource, apply the associations fields there. For instance:

render json: @videos.limit(1), each_serializer: VideoSerializer,  fields: [:id, user: [:name]]

Then this should apply the fields: [:id] only in the VideoSerializer and if there is a user association, apply fields: [:name] in the UserSerializer.

I am really looking forward to merge this PR so if you need anything from me just ping me. I will also create a regression test highlighting the bug.

@NullVoxPopuli
Copy link
Contributor

@vasilakisfil can you fix the errors mentioned in travis? thanks!

Add optional fields support for associations

wip

fix tests

fix styling
@vasilakisfil
Copy link
Contributor Author

@NullVoxPopuli fixed in travis, waiting for appveyor which takes some time..
Possibly linked to #1285 and #1286

@beauby
Copy link
Contributor

beauby commented Oct 20, 2015

So if we're going down this way you should make use of the IncludeTree class that does exactly what you want, plus it can be built from a string and an extended array/hash syntax.

relationships[association.key] = relationship_value_for(association, options)
relationships[association.key] = relationship_value_for(
association,
options.merge(fields: association_fields_hash[association.name])
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you do that, not providing the fields option to the adapter will result in no attribute being serialized.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

not the case since this runs only for associations. The case you said would only happen with the following (given that there is a user association):

render json: @videos.limit(1), each_serializer: VideoSerializer,  fields: [:id, user: []]

Plus, test would cry out if that was true.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry that was an oversight.

@vasilakisfil
Copy link
Contributor Author

@beauby "So if we're going down this way you should make use of the IncludeTree class that does exactly what you want, plus it can be built from a string and an extended array/hash syntax."

I never considered AMS job to parse the query string (+ strings are not the way to go in Ruby but hashes/arrays). Especially when we are talking about JSON adapter. But sure I can fix it.

What do you mean extended array/hash syntax ?

@beauby
Copy link
Contributor

beauby commented Oct 22, 2015

@vasilakisfil The query string parsing is here just as a commodity for people dealing with JSON API stuff. The "extended array/hash syntax" is the one described here. Basically it's an array of hashes and symbols (S → Array<Symbol | Hash{Symbol:S}>).

@vasilakisfil
Copy link
Contributor Author

@beauby the array/hash syntax is supported in this pull request: fields: [:id, { user: [:name] }]. Query string was not supported before this pull request. Do you want me to add support for it?

I am totally against it though because unless you are writting an internal API, you will want to filter the query params (query string) beforehand using StrongParams or something else inside the controller so you would need to convert it to an array of hashes/symbols anyway. Extract a helper method that parses this query string in the controller is the way to go in my opinion.

@passalini
Copy link

+1
ps: nice idea @vasilakisfil

def test_fields_option
serializer = ArraySerializer.new([@first_post, @second_post])
adapter = ActiveModel::Serializer::Adapter::Json.new(serializer)
actual = adapter.serializable_hash(fields: [:id])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looking good

  1. this needs an integration (controller) test
  2. Please use ActiveModel::SerializableResource.new([@first_post, @second_post], fields: [:id])

Please don't test passing options into serializable_hash since only the renderer would do that

Which is to say, this isn't testing how the library is used

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(that would be a better test usage in the integration test, where the options to render json: foo, .... are passed to AMS then to the renderer (unless we mutate them))

@vasilakisfil Are you still interested in this?

@vasilakisfil
Copy link
Contributor Author

@bf4 hmm I am not sure. I have already created my own adapter that is compatible with 0.9.x version and supports association fields among others.

I could work on that but I feel it's kind of deadend. Most attributes/json-adapter related code hasn't been updated for a long time now and most committers in this project doesn't seem to be interested to extend json/attributes adapter features but instead try to keep them as minimal as possible and try to spend more time to jsonapi adapter (which might be a good idea until Rails 5 comes out). That's why I opted for a custom adapter to move faster.

@bf4
Copy link
Member

bf4 commented Dec 23, 2015

custom adapter is good. I'm not sure what you mean by extending the json/attributes adapters. Do you mean improving or otherwise working on?

@williamweckl
Copy link

+1

@bf4
Copy link
Member

bf4 commented Feb 26, 2016

I have already created my own adapter that is compatible with 0.9.x version and supports association fields among others.

@vasilakisfil Is that something you could share? Would be very helpful.

Any interest in reviving this PR?

@NullVoxPopuli
Copy link
Contributor

@vasilakisfil could you rebase this? :-)

@NullVoxPopuli
Copy link
Contributor

I'd like to get this merged before taking a look at #1286

@vasilakisfil
Copy link
Contributor Author

Sure I would love to rebase it and give some love to the JSON/Attributes adapters but not now during Easter (1.5 week from now) as I am super busy currently.

I have created 4-5 monkey patched eventually to make it work like GraphQL, didn't create my own adapter.

@bf4
Copy link
Member

bf4 commented Mar 16, 2016

@vasilakisfil

I have created 4-5 monkey patched eventually to make it work like GraphQL, didn't create my own adapter.

would love to see them!

@vasilakisfil
Copy link
Contributor Author

Ok if it helps, the following have been tested up until b2cd7bb (the git commit locked in my Gemfile).

There is one more bug (I think it's a bug and not intended) down the line (in ActiveModel::Serializer::Associations#association method), so this pull request doesn't actually solves it. But as I said I think JSON/Attributes adapters have been slightly neglected by the team in favor of JSONAPI adapter and I would love to help out.

class ActiveModel::Serializer::Adapter::Attributes
  def relationship_value_for(association, options)
    return association.options[:virtual_value] if association.options[:virtual_value]
    return unless association.serializer && association.serializer.object

    opts = instance_options.merge(
      include: @include_tree[association.key],
      fields: nil
    )
    self.class.new(association.serializer, opts).serializable_hash(
      options.merge(fields: nil)
    )
  end

  def serializable_hash(options = nil)
    options ||= {}
    options[:fields] = instance_options[:fields] if options[:fields].nil?

    if serializer.respond_to?(:each)
      serializable_hash_for_collection(options)
    else
      serializable_hash_for_single_resource(options)
    end
  end

  def serializable_hash_for_collection(options)
    serializer.map { |s|
      self.class.new(s, instance_options).serializable_hash(options)
    }.select{|s| !s.blank?}
  end

  def serializable_hash_for_single_resource(options)
    return {} if serializer.invalid_for_serialization?

    resource = resource_object_for(options)
    relationships = resource_relationships(options)
    resource.merge!(relationships)
  end
end
module ActiveModel
  class Serializer
    module Associations
      def associations(include_tree = DEFAULT_INCLUDE_TREE)
        return unless object

        Enumerator.new do |y|
          self.class._reflections.each do |reflection|
            key = reflection.options.fetch(:key, reflection.name)
            next unless include_tree.key?(key)
            y.yield reflection.build_association(
              self, instance_options.merge(fields: nil)
            )
          end
        end
      end
    end
  end
end

class ActiveModel::SerializableResource
  def initialize(resource, options = {})
    @resource = resource
    @adapter_opts, @serializer_opts =
      options.partition { |k, _| ADAPTER_OPTION_KEYS.include? k }.map { |h| Hash[h] }
    @serializer_opts.merge!(fields: options[:fields])
  end
end

class ActiveModel::Serializer
  def attributes
    attributes = self.class._attributes.dup

    attributes.each_with_object({}) do |name, hash|
      next unless instance_options[:fields].nil? || field_allowed?(name)

      if self.class._fragmented
        hash[name] = self.class._fragmented.public_send(name)
      else
        hash[name] = send(name)
      end
    end
  end
  def field_allowed?(name)
   instance_options[:fields] && instance_options[:fields].include?(name)
  end
end

@vasilakisfil
Copy link
Contributor Author

The last monkeypatch is irrelevant actually, it allows you to take out an item from a collection just before serializing.

@vasilakisfil
Copy link
Contributor Author

No sorry, that's wrong, I don't even remember my own monkey patches haha. The last one is something else I think but the invalid_for_serialization? part does what I said :)

Anyway :) But I would be happy to commit to this project.

@remear
Copy link
Member

remear commented Sep 16, 2016

Closing due to inactivity

@remear remear closed this Sep 16, 2016
@NullVoxPopuli
Copy link
Contributor

I also believe this functionality already exists

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants