-
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
ActiveModel::Serializer::type now accepts a block #1399
Conversation
end | ||
inflection = ActiveModelSerializers.config.jsonapi_resource_type | ||
serializer.object.class.model_name.public_send(inflection) |
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 structure of this code change makes me a bit uncomfortable. Since the change is that type
is a value or a callable, I'd think that either the value is also made callable in the setter, or that it is consumed with a respond_to?(:call) and no more-- I'm surprised to see so many changes both here and in the tests. I'm going to take a closer look when I have some time. Would you mind sharing your design thoughts?
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.
Concerning your first point, I based my change on the link
method and how it is used in the adapter (https://github.com/groyoh/active_model_serializers/blob/dynamic_type/lib/active_model/serializer/adapter/json_api.rb#L214). I'd be happy to change it so that the setter creates a callable out of the value if you consider that's a win.
Concerning the changes I made in this function they were mainly DRYing up the code, but I'll revert it to keep only the necessary code (and maybe create another PR for the unnecessary changes).
Concerning the changes in the specs, I mainly refactored the code to use a helper method to avoid repeating the code everywhere in the test file and also added specs to test that using the method in a subclass doesn't break it in the superclass.
A block passed to the type method will be instance_eval'd to the serializer. This allows us to avoid overriding the #_type method to have a dynamic type.
end | ||
|
||
private | ||
|
||
def with_jsonapi_resource_type type |
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.
Simply moved this method to be private.
return serializer._type if serializer._type | ||
if ActiveModelSerializers.config.jsonapi_resource_type == :singular | ||
serializer.object.class.model_name.singular | ||
value_or_block = serializer._type |
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 I'd rather see in implementation more like in #1370 and #1383 and note https://github.com/rails-api/active_model_serializers/pull/1370/files#r48116422 hasn't yet be resolved.
cc @beauby
Does that make sense? There's a lot going on in this method because there's uncertainty about how to handle the value being passed way down.
Could you describe a use-case where the type of a resource should be dynamically resolved? I'm guessing some polymorphic associations but I'm not sure exactly. |
@beauby indeed, I was thinking of polymorphic classes where the attributes and associations are the same and where the client should handle the resource differently depending of the type. |
@groyoh Oh right, you mean polymorphic classes and not polymorphic associations. Could you sketch a real-life example to help me understand your use-case properly? |
@beauby I'll give you a simple example that does not necessarily involve polymorphism. If for example you want to define a special inflection by defining a class BaseSerializer < ActiveModel::Serializer
type do
object.class.name.camelize
end
# Right now you'd have to do it like so
# def _type
# object.class.name.camelize
# end
end
class SomeSerializer < BaseSerializer
end This will allow you to avoid defining a static type within every serializer. |
I see. The use-case you're mentioning (globally setting the format of the type) should be taken care of by some pending PRs (ref missing). Polymorphic associations should work out of the box as the correct serializier would be inferred. The only possible case left would be something like: class AbstractObject
...
end
class ConcreteObject1 < AbstractObject
...
end
class ConcreteObject2 < AbstractObject
...
end
class AbstractObjectSerializer < ActiveModel::Serializer
...
end where the same serializer would be used to serialize different kinds of objects. What I'm wondering is whether this situation wouldn't be better addressed by using polymorphic associations instead of polymorphism at the object level. So I'd really like to see your use-case. |
Let's say you have a legacy system (that you can't change) used to track a statistics of a fair over the time. You want to track the number of incoming visitors, outgoing visitors, number of tweets, etc. and sample these values as a {
"data": [
{
"id": "1234",
"type": "incoming_vistors",
"attributes": {
"samples": "a certain encoded string"
}
},
{
"id": "1234",
"type": "outgoing_visitors",
"attributes": {
"samples": [
{
"timestamp": 1231241241,
"value": 56
}
]
}
}
]
} To solve this, I would use a single model and serializer that take the Another way to solve this issue would be to combined the I hope my example was clear enough. |
@groyoh For the sake of clarity, would you mind sketching the db schema for your |
Table: fair_statistics
Model: class FairStatistic < ActiveRecord::Base
end Serializer: class FairStatisticSerializer < ActiveModel::Serializer
type do
object.type
end
attributes :created_at, :updated_at, :samples
end |
So a direct workaround for your situation would be to have attribute :type, key: :statistic_type so that your client would have full information (under the |
@beauby thanks for the proposal but the issue that I stated within #1399 (comment) is that every statistic have the same id as the fair (for legacy reason) but the primary key is the combination of id and type. So if I do as you stated, I might have multiple statistics with the same jsonapi {
"data": [
{
"id": "1234",
"type": "fair_statistic",
"attributes": {
"statistic_type": "incoming_vistors",
"samples": "a certain encoded string"
}
},
{
"id": "1234",
"type": "fair_statistic",
"attributes": {
"statistic_type": "outgoing_vistors",
"samples": [
{
"timestamp": 1231241241,
"value": 56
}
]
}
}
]
} Sorry for not being clear enough once again. |
Something in your scenario doesn't make sense. Two records can share the same type and id?
|
So type is an attribute, right? B mobile phone
|
Two records can share the same This is why the only two solution I see is:
|
@groyoh So basically your |
@beauby the primary key is a composed primary key using the columns |
Is this sti? Sounds like you're overloading the word 'type' to mean both the record's type and one of its attributes. B mobile phone
|
@bf4 I wouldn't say it is sti as there is only one table and one model. AFAIK, sti would imply to have one model per 'type'. |
Can you maybe describe your record/model without using the word type? Anf how it might represent two Distinct API resources? B mobile phone
|
I'll remove the use of CREATE TABLE fair_statistics
(
fair_id INTEGER,
category VARCHAR(255),
created_at TIMESTAMP,
updated_at TIMESTAMP,
samples BLOB,
CONSTRAINT pk_fair_statistics PRIMARY KEY (fair_id, category)
) The INSERT INTO fair_statistics VALUES (1, "ingoing_visitors", now(), now(), some_binary_data);
INSERT INTO fair_statistics VALUES (1, "outgoing_visitors", now(), now(), some_binary_data); Within the JSONAPI spec, the class FairStatistic < ActiveRecord::Base
# some logic for the `samples` attribute
end
class FairStatisticSerializer < ActiveModel::Serializer
attributes :created_at, :updated_at, :samples
type do
object.category
end
def id
object.fair_id
end
end
{
"data": [
{
"id": "1",
"type": "outgoing_visitors",
"attributes": {
"created_at": "2016-01-14T16:15:09+00:00",
"updated_at": "2016-01-14T16:15:09+00:00",
"samples": "some encoded string"
}
},
{
"id": "1",
"type": "ingoing_visitors",
"attributes": {
"created_at": "2016-01-14T16:15:09+00:00",
"updated_at": "2016-01-14T16:15:09+00:00",
"samples": "some encoded string"
}
}
]
} The workaround to avoid using a dynamic type would be to concatenate the class FairStatistic < ActiveRecord::Base
# some logic for the `samples` attribute
end
class FairStatisticSerializer < ActiveModel::Serializer
attributes :created_at, :updated_at, :samples
# type "fair_statistics"
def id
"#{object.fair_id}-#{object.category}"
end
end {
"data": [
{
"id": "1-outgoing_visitors",
"type": "fair_statistics",
"attributes": {
"created_at": "2016-01-14T16:15:09+00:00",
"updated_at": "2016-01-14T16:15:09+00:00",
"samples": "some encoded string"
}
},
{
"id": "1-ingoing_visitors",
"type": "fair_statistics",
"attributes": {
"created_at": "2016-01-14T16:15:09+00:00",
"updated_at": "2016-01-14T16:15:09+00:00",
"samples": "some encoded string"
}
}
]
} The latter is rather ugly and not convenient as it implies to define a specific concatenation/splitting to the |
@groyoh gotcha. So, the issues are:
So, I think part of the problem here is that we want to refer to a FairStatistic by its category, not by its model name. When you're talking about it, how do you refer to it? Do you call it an IncomingVisitorStatistic? |
👍 Nice recap @bf4 |
possibly related to #1420 |
@bf4 thanks for the recap. Indeed it would be referred as |
Closing this for now. I'll reopen with more info if feel there is a real need for it 😉 |
A block passed to the type method will be instance_eval'd to the serializer.
This allows us to avoid overriding the #_type method to have a dynamic type.