-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Conversation
@vasilakisfil can you fix the errors mentioned in travis? thanks! |
Add optional fields support for associations wip fix tests fix styling
@NullVoxPopuli fixed in travis, waiting for appveyor which takes some time.. |
So if we're going down this way you should make use of the |
relationships[association.key] = relationship_value_for(association, options) | ||
relationships[association.key] = relationship_value_for( | ||
association, | ||
options.merge(fields: association_fields_hash[association.name]) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
@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 ? |
@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}>). |
@beauby the array/hash syntax is supported in this pull request: 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. |
+1 |
def test_fields_option | ||
serializer = ArraySerializer.new([@first_post, @second_post]) | ||
adapter = ActiveModel::Serializer::Adapter::Json.new(serializer) | ||
actual = adapter.serializable_hash(fields: [:id]) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking good
- this needs an integration (controller) test
- 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
There was a problem hiding this comment.
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?
@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. |
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? |
+1 |
@vasilakisfil Is that something you could share? Would be very helpful. Any interest in reviving this PR? |
@vasilakisfil could you rebase this? :-) |
I'd like to get this merged before taking a look at #1286 |
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. |
would love to see them! |
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 |
The last monkeypatch is irrelevant actually, it allows you to take out an item from a collection just before serializing. |
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 Anyway :) But I would be happy to commit to this project. |
Closing due to inactivity |
I also believe this functionality already exists |
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:
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:
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:
Then this should apply the
fields: [:id]
only in the VideoSerializer and if there is a user association, applyfields: [: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.