Description
Introduction
I've been having a lot of confusion and difficulty implementing something that feels like it should be simple, and it's led to me tracing across a lot of issues & PR's while trying to figure it out. In the process of doing this, I get the impression everyone is collectively also trying to figure it out and nobody's quite satisfied with how things are. So, I had an idea for how I'd like things to be, and figured I'd type it out to see if it made any sense or be helpful.
Other Relevant discussions
The best articulation is right here: #599 (comment)
but also #1252 and #1506 and #1711
Problem I'm Running Into
I'm building app where people can track gift ideas. So they can add their own, but their friends and family can add gift ideas for them, too. I have a gift model:
class Gift < ApplicationRecord
belongs_to :recipient, class_name: 'User', foreign_key: :recipient_id
belongs_to :author, class_name: 'User', foreign_key: :author_id, required: false
enum status: { requested: 0, redeemed: 1, trashed: 2, error: 3 }
end
And a really sensible endpoint for this is:
/api/users/:id/gifts
The issue is, this endpoint should send back different data based on 3 situations:
1. "Author" - User is looking at their own list
Some things I'd want to include here but not elsewhere:
- trashed gifts added by the user (in case they change their mind)
Some things I wouldn't want to include here:
- gifts suggested by friends
- the status of any gift (so they don't know if it's purchased)
2. "Friends" - A user is looking at their friend's list
Some things I'd want to include here but not elsewhere:
- the status of everything (so two people don't buy the same gift)
- gifts suggested by friends
Some things I wouldn't want to include here:
- the trashed gifts added by friends (so they could change their mind)
3. "Public" - Non-friend users or someone who isn't logged in
Some things I wouldn't want to include here:
- any sort of status of any gift
- any gifts not added by that user
- any trashed gifts
basically a real pared down version with minimal info
What I wish I could do
I wish I could set up one serializer per resource, then define the rules
for the different ways I need to serialize that data up in. Being able to just implicitly use the GiftSerializer
is really nice, and I wish I could also compose the rules
in that serializer.
# app/api/controllers/gifts_controller
class Api::GiftsController < Api::ApiController
include UserScoped # sets @user
def index
@gifts = Gift.recipient_is(@user.id).author_is(@user.id)
render json: @gifts, context: :author
end
end
And then in the serializer,
class GiftSerializer < ActiveModel::Serializer
attributes :id, :name, :note, :link, :position, :status, :recipient_id, :author_id, :created_at, :updated_at
module Author
# define the base set of attribute that can get sent up, included, filtered, etc
end
end
Now, there are some things I don't want my serializer to do:
- any sort of authorization logic
- any sort of scoping or filtering
- any sort of custom querying unrelated to composing an attribute
I want to specify the data that I want to send, then how that will be sent up. So, ideally, I could do something like this:
# app/api/controllers/gifts_controller
class Api::GiftsController < Api::ApiController
include UserScoped
def index
case
when is_author?
@gifts = Gift.recipient_is(@user).author_is(@user)
rule = :author
when is_friend?
@gifts = Gift.recipient_is(@user)
rule = :friends
else
@gifts = Gift.recipient_is(@user.id)
rule = :public
end
render json: @gifts, context: :public, rule: rule
end
private
def is_author?
current_user && current_user == @user
end
def is_friend?
current_user && @user.friends.include?(current_user)
end
end
So then, in my serializer, I could compose everything there:
class GiftSerializer < ActiveModel::Serializer
attributes :id, :name, :note, :link, :position, :status, :recipient_id, :author_id, :created_at, :updated_at
module Author
# restrict status & non-author added gifts
end
module Friend
# this should be everything by default
end
module Public
# restrict status
end
end
Note: This serializer doesn't take care of what trashed gifts to include or not, but it shouldn't care. I should be handling that in the controller, because it's a decision about what data is getting serialized. This is a requirement that could easily slip over into the serializer (filtering what data gets sent up), but I shouldn't be able to easily do that.
I specify the data, and the serializer sends up the right view of that data. I just have module there, but this isn't an actual implementation thing. Really what I want is the ability to start building out these different rulesets in a single file, and then break it out when things get more complex over time:
/app/serializers/gift_serializer.rb
/app/serializers/gifts/author.rb
/app/serializers/gifts/friend.rb
/app/serializers/gifts/public.rb
And then keep what's going on sensibly explicit inside the serializer:
class GiftSerializer < ActiveModel::Serializer
# different potential rules for serializing are defined here
include Gifts::Author
include Gifts::Friend
include Gifts::Public
# defaults are here
end
Additional Thoughts
Naming
Initially, instead of "rule" or "rules" I thought a great word for serializing data differently in different situations would be "context," but that's already taken by ruby. I think the concept of a Rule
makes sense now.
Usability
I know I can make more serializers, but I'd love to not have to make a new serializer for minor differences in what I want to send up per resource. As I've outlined here, I'd prefer everything to flow through the GiftSerializer
, and be able to break that out when stuff gets too hairy.
As for each different "ruleset," might be nice to have:
- default pulls all attributes specified in base serializer
- easy ways to diff against it: include certain keys, don't include certain keys, or just specify a new list (although this is probably a sign you'd want a new serializer)
- ability to override custom attribute definitions
- still have access to the arbitrary options you can already pass in
I understand what I've outlined here might be crazy, but I'd love to be able to use AMS this way, and be guided there naturally both by how I naturally develop and the documentation here. This totally could be a completely terrible idea/approach, so in that case I'd like to know why. But otherwise, if this seems like a generally attractive direction, I'd be happy to make an attempt at PR.
Thanks,
Chris