0

I'm trying to create a hypermedia api in rails. I'd like to serialize my payloads with active_model_serializers using the json_api adapter. But it doesn't seem trivial to serialize links conditionaly.

It's kind of a blog application where users can follow other users. So when I serialize a User resource, say for UserA, I want to have a link with rel :follow if current_user is not following UserA and a link with rel :unfollow if current_user is already following UserA. This seems like an extremely trivial use case when creating a hypermedia api. Does anyone know if there's any good way of doing this with active_model_serializers?

I currently wrote something like this (and include it in all serializers):

def self.link(rel, &block)
      serializer = self
      super do
        user = scope
        next unless serializer.can?(user, rel, @object)
        instance_eval(&block)
      end
    end

# And in serializer (just as usual):

link :self do
  api_user_path(object.id)
end

It does work. But it just don't feel right. And I wouldn't be surprised if future changes to active_model_serializers screw things up for me.

sammygadd
  • 299
  • 2
  • 6

1 Answers1

0

If someone else is looking for a solution to this here is what I did. I added the gem Pundit and made the Policy classes in charge of link serialization (as well as the usual authorization) by adding methods called "link_#{rel}". I created a base serializer like this:

module Api
  class BaseSerializer < ActiveModel::Serializer
    include Pundit

    def self.link(rel, &block)
      unless block_given?
        Rails.logger.warn "Link without block (rel '#{rel}'), no authorization check"
        return super
      end
      method = "link_#{rel}"
      # We need to let the super class handle the evaluation since
      # we don't have the object here in the class method. This block
      # will be evalutated with instance_eval in the adapter (which has
      # the object to be serialized)
      super do
        policy_class = PolicyFinder.new(object).policy
        unless policy_class
          Rails.logger.warn "Could not find policy class for #{object.class}."
          next
        end
        user = scope
        policy = policy_class.new(user, object)
        unless policy.respond_to?(method)
          Rails.logger.warn "Serialization of #{object.class} infers link with rel '#{rel}'. " \
            "But no method '#{method}' in #{policy.class}."
          next
        end
        next unless policy.public_send(method)
        instance_eval(&block)
      end
    end

  end
end

Then other serializers inherit from BaseSerializer, like:

module Api
  class UserSerializer < BaseSerializer
    type 'user'
    attributes :name,
               :email,
               :followers_count,
               :following_count,
               :created_at,
               :updated_at

    link :self do
      api_user_url(object)
    end

    link :edit do
      api_user_url(object)
    end

    link :follow do
      follow_api_user_url(object)
    end

    link :unfollow do
      unfollow_api_user_url(object)
    end
  end
end

So the Policies are just like normal Pundit Policies with some added methods for each link that should be serialized (or not).

class ApplicationPolicy
  attr_reader :user, :record

  def initialize(user, record)
    @user = user
    @record = record
  end

  def link_self
    true
  end

end

module Api
  class UserPolicy < ApplicationPolicy

    alias current_user user
    alias user record

    def link_edit
      current_user && current_user.id == user.id
    end

    # show follow link if user is not current_user and
    # current_user is not already following user
    def link_follow
      current_user && current_user.id != user.id && !current_user.following?(user)
    end

    # show follow link if user is not current_user and
    # current_user is following user
    def link_unfollow
      current_user && current_user.id != user.id && current_user.following?(user)
    end
  end
end
sammygadd
  • 299
  • 2
  • 6