-
Notifications
You must be signed in to change notification settings - Fork 1.4k
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
Add inline syntax for attributes and associations #1356
Conversation
serialized_associations[reflection_name] = ->(instance) { instance.object.send(reflection_name) } | ||
end | ||
|
||
define_method reflection_name do |
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.
Is there still a need/reason to define such methods on the serializer?
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.
No, except that it breaks some tests, and I didn't want to go and fix 'em :)
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.
So, we can do it in two steps
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.
Yeah that's what was holding me back, because even after fixing the tests, there are caching issues (I believe caching is broken tbh).
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.
oh, and re having a serializer/attributes.rb file, like in your prs, .. I'm pro moving more stuff in there (though I think it should be a class), but even the current Struct isn't really necessary now. we can put that off to future work
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.
The idea behind serializer/attributes.rb
was mainly to separate various serializer concerns into their own modules/files. The goal was that serializer.rb
would contain as little stuff as possible and that the logic for handling attributes-related stuff would be in one file (class and instance methods).
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.
Yeah, I started on it, but it wasn't worth the extra effort in this PR. That file is sitting on my file-system
if block_given? | ||
serialized_attributes[key] = Attribute.new(->(instance) { instance.instance_eval(&block) }) | ||
else | ||
serialized_attributes[key] = Attribute.new(->(instance) { instance.object.read_attribute_for_serialization(attr) }) |
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.
Why encapsulate the lambda in an Attribute
object? (Especially considering we're not doing the same for associations)
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.
no reason right now. I was thinking I'd do more with it, but I didn't. So, can be removed.
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.
Yeah, I think we talked about it and I agree that at some point it would be nice, but at the moment I think it just adds complexity.
1de7489
to
011b589
Compare
rebased and force pushed. I don't think I broke anything... |
011b589
to
4bdf44a
Compare
It's not clear to me that this PR satisfies goal 1 (Allow defining attributes so that they don't conflict with existing methods). It looks like trying to add a key whose name is already a method name on the serializer will add an entry to serialized_attributes, but that it still delegates to the existing method. I tried to get around this in #1354 by avoiding the usage of the key name as the method name, but it seems like there is room for improvement over that here in evaluating the proc directly instead of (or in addition to?) adding methods to the serializer. |
attribute :name do | ||
"#{object.first_name} #{object.last_name}" | ||
end | ||
end |
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.
@noahsilas I do want to remove the dynamic methods on the serializer, just not in this PR.
However, I do think it's a good idea for there to be a test against attribute names/keys that conflict with methods on the serializer. I think the read_attribute_for_serialization
should get around that
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.
I mean, right now it is
self.class._fragmented ? self.class._fragmented.public_send(key) : send(key)
but it could be
self.class._fragmented ? self.class._fragmented.public_send(key) : object.read_attribute_for_serialization(key)
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.
I think that the problem here is that the key may not be the name of the attribute that we want to read, as in:
class MyThingSerializer < ActiveModel::Serializer
attribute :name, key: :object
end
This should render something like { "object": "my name" }
, but to get that, we actually need to invoke the read the name
method on the objectname
attribute for serialization.
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.
I should add a case here using the virtual_value
option. e.g. the poro in our fixtures file
class VirtualValueSerializer < ActiveModel::Serializer
attributes :id
has_many :reviews, virtual_value: [{ id: 1 }, { id: 2 }]
end
If you want, I can rebuild #1354 on top of this change and submit a new PR, separating the goal of block definition from the goal of avoiding namespace conflicts. |
You can re-use this pr Take it away (please)! B mobile phone
|
I'm not sure that I can push onto this PR, but rebasing #1354 onto this branch ends up looking like this: https://github.com/brigade/active_model_serializers/commits/attribute_objects (specifically, see brigade/active_model_serializers@ca76be1a46 ). There is still some weird interplay with defining methods on the serializer so that the old style method overrides still work, which is a little disappointing, but a major break in the API, so I didn't feel comfortable removing it. |
@noahsilas re: 'you can re-use this pr', my mistake. I was replying from email and thought it was the one you authored. I was thinking that a way to get rid of the dynamic methods would be to pretend they're there. Maybe with a respond_to_missing, or in the 'reader' |
33513fc
to
eb6850e
Compare
@noahsilas I added your commit from that branch and made one major and a few minor changes
The PR description is now out of date |
@@ -14,6 +14,9 @@ class Serializer | |||
include Configuration | |||
include Associations | |||
require 'active_model/serializer/adapter' | |||
Attribute = Struct.new(:name, :reader) do | |||
delegate :call, to: :reader | |||
end |
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.
See #1356 (comment)
I could go either way on this.
class << base | ||
attr_accessor :_reflections | ||
included do | ||
with_options instance_writer: false, instance_reader: true do |serializer| |
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.
I don't know if there's a difference between the block variables base
and serializer
in what we want here.
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.
Unclear to me as well.
@groyoh @beauby The relationship links work seems like it is primarily applicable to the JsonApi adapter; is there a way to satisfy this desire that doesn't introduce overhead for all other adapters? (I may be misunderstanding, but having to specify the relationship via a |
True, but it's not in this PR :) |
|
||
private | ||
|
||
def _serializer_instance_methods |
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.
Wouldn't that be better as a class method performance wise?
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.
I think you're right. I was working on the instance-level and got tunnel-vision
|
||
def self.build_reader(name, block) | ||
if block | ||
->(instance) { instance.instance_eval(&block) } |
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.
I'm looking at 0.8 and how JSON API considers associations 'includes' and am thinking that association block should be applied to the association, rather than the serializer. Otherwise, there's really nothing that ties the block to an association.
So, usage from current impl would be
has_many :comments do
- comments.limit(5)
+ limit(5)
end
which is also more inline with how the scope
method works in Rails. I think this makes the api a lot more clear. If you want a custom attribute, use the attribute block. If you want a scoped association, I think it the instance_eval should be on the association.
See
def associate(klass, attrs) #:nodoc:
options = attrs.extract_options!
self._associations = _associations.dup
attrs.each do |attr|
unless method_defined?(attr)
define_method attr do
object.send attr
end
end
define_include_method attr
self._associations[attr] = klass.refine(attr, options)
end
end
def define_include_method(name)
method = "include_#{name}?".to_sym
INCLUDE_METHODS[name] = method
unless method_defined?(method)
define_method method do
true
end
end
end
def include_associations!
_associations.each_key do |name|
include!(name) if include?(name)
end
end
def include?(name)
return false if @options.key?(:only) && !Array(@options[:only]).include?(name)
return false if @options.key?(:except) && Array(@options[:except]).include?(name)
send INCLUDE_METHODS[name]
end
def include!(name, options={})
EMBED_IN_ROOT_OPTIONS = [
:include,
]
def associations(options={})
associations = self.class._associations
included_associations = filter(associations.keys)
associations.each_with_object({}) do |(name, association), hash|
if included_associations.include? name
# ...
end
def filter(keys)
if @only
keys & @only
elsif @except
keys - @except
else
keys
end
end
# Notice that the association block is evaluated in the context of the association. | ||
# Specifically, the association 'comments' is evaluated two different ways: | ||
# 1) as 'comments' and named 'comments'. | ||
# 2) as 'comments.last(1)' and named 'last_comments'. |
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.
See comment https://github.com/rails-api/active_model_serializers/pull/1356/files#r46723777 about how we should think of association blocks (and how we might want to also consider them as only used when included under JSON API) and see #1325 (comment)
@noahsilas I'm pretty sure we can work around this issue. For example, making sure that
has_many :comments
has_many :comments do
object.special_comments
end
has_many :comments do
link :self do
#something
end # => :SpecialSymbolToPreventInlineAssociation
end
has_many :comments do
link :related do
#something
end
object.association # or data object.association
end The first two would work pretty well with any adapter while the last one would only fit JSONAPI and everybody is happy 😁 . Of course, this probably require more thoughts and maybe have some flaws too. |
@@ -17,7 +25,28 @@ class Serializer | |||
# | |||
# So you can inspect reflections in your Adapters. | |||
# | |||
Reflection = Struct.new(:name, :options) do | |||
Reflection = Struct.new(:name, :options, :block) do | |||
delegate :call, to: :reader |
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.
Why delegating it rather than calling @reader.call
in value
?
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.
I just think it reads better. It might be a performance issue.
I think there's definitely room for refactoring and revising this, but I don't think it should be a blocker. I don't really like how the reflection differs from the attribute, but figure we can kick that can down the road a bit more into its own PR.
@rails-api/ams Anything blocking merging this? I'll merge in 24 hours unless I hear otherwise, since it seems pretty well-discussed, and we can always make changes later |
@bf4 I think it'll be a good addition :-) |
d3837f9
to
bf8270b
Compare
Add inline syntax for attributes and associations
Goals:
has_many :comments do last(5) end
virtual_value
such ashas_many :reviews, virtual_value: [{ id: 1 }, { id: 2 }]
and how association virtual values differ from attribute values.
Based on work in #1262, #1354
Ref: #1263, #1261, #1311
This PR is intended as a starting point for further work improving how
serializers map their resources attributes and associations
Introduced, in support of this:
Serializer#_attribute_mappings
Possible regression:
removed referring to object attributes in the serializer without calling them directly on object. (I didn't even know this was possible. We could add a respond_to_missing / method_missing for this)
Associations as 'includes'
It seems like we might benefit from thinking of associations, at least in the JSON API adapter, as includes, and only including them when requested (which is also similar to earlier naming and usage in the library, which I like). This PR made me think of it, in part, because the difference between a reflection, an association, and associations, I think should be more clear (i.e. just an include).
Blocks in serializer attribute definitions
I like this. They remove the need for defining methods on the serializer, and make these attributes seem more like transformations. I don't mind that they require a call to object, but would be okay eval'ing them in the context of the object, instead of the serializer.
attribute :name do 'AMS' end
Blocks in serializer association definitions
Since there may be more options to pass in here, and to distinguish the context of an association block, which is on the association, from the attribute block, which is on the serializer, I'm tossing around the idea of having a
scope
option the receives a proc or lambda. e.g.has_many :comments, ->{ last(5) }
(NOT->{ comments.last(5) }
has_many :comments, scope: ->{ last(5) }