Update GET /followers API endpoint (#13287)

* Add created_at to API followers controller

* Add sorting

* Update API docs

* Update app/controllers/api/v0/followers_controller.rb

Co-authored-by: Jamie Gaskins <jgaskins@gmail.com>

* Update app/controllers/api/v0/followers_controller.rb

Co-authored-by: Jamie Gaskins <jgaskins@gmail.com>

* Use correct date format

* Update API docs

* Add JsonApiSortParams concern

* Update controller to use concern

* Rename and add docs

* Update API docs

* Remove leftover file

* Undo gem changes

* Consistently use field vs param

* Reset vendor/cache for git dependencies

Co-authored-by: Jamie Gaskins <jgaskins@gmail.com>
This commit is contained in:
Michael Kohl 2021-04-13 10:12:42 +07:00 committed by GitHub
parent a5e572db21
commit d1c3380161
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 122 additions and 8 deletions

View file

@ -1,11 +1,13 @@
module Api
module V0
class FollowersController < ApiController
include JsonApiSortParam
before_action :authenticate_with_api_key_or_current_user!
before_action -> { limit_per_page(default: 80, max: 1000) }
USERS_ATTRIBUTES_FOR_SERIALIZATION = %i[
id follower_id follower_type
id follower_id follower_type created_at
].freeze
private_constant :USERS_ATTRIBUTES_FOR_SERIALIZATION
@ -13,7 +15,7 @@ module Api
@follows = Follow.followable_user(@user.id)
.includes(:follower)
.select(USERS_ATTRIBUTES_FOR_SERIALIZATION)
.order(created_at: :desc)
.order(order_criteria)
.page(params[:page])
.per(@follows_limit)
end
@ -24,6 +26,14 @@ module Api
per_page = (params[:per_page] || default).to_i
@follows_limit = [per_page, max].min
end
def order_criteria
parse_sort_param(
params[:sort],
allowed_fields: [:created_at],
default_sort: { created_at: :desc },
)
end
end
end
end

View file

@ -0,0 +1,32 @@
module JsonApiSortParam
# Handles JSON API style sort params
#
# @param param_string [String] The unmodified request param
# @param allowed_fields [Array<Symbol>] Fields that can be sorted by. This
# array determines the sort order of the resulting hash.
# @param default_sort [{Symbol => Symbol}] The default sort order. Used when
# the param string is nil/empty or when parsing it results in an empty hash.
def parse_sort_param(param_string, allowed_fields:, default_sort:)
fields = param_string.to_s.split(",")
unfiltered_hash = fields_to_hash(fields)
sort = sort_and_filter(unfiltered_hash, allowed_fields)
sort.presence || default_sort
end
private
def fields_to_hash(fields)
fields.each_with_object({}) do |field, result|
if field.start_with?("-")
result[field[1..]] = :desc
else
result[field] = :asc
end
end.symbolize_keys
end
def sort_and_filter(fields_hash, allowed_fields)
field_order = allowed_fields.each_with_index.to_h
fields_hash.slice(*allowed_fields).sort_by { |k, _v| field_order[k] }.to_h
end
end

View file

@ -1,6 +1,7 @@
json.array! @follows do |follow|
json.type_of "user_follower"
json.id follow.id
json.type_of "user_follower"
json.id follow.id
json.created_at utc_iso_timestamp(follow.created_at)
json.partial! "api/v0/shared/follows", user: follow.follower
end

View file

@ -687,6 +687,7 @@ components:
type: object
required:
- type_of
- created_at
- id
- name
- path
@ -695,6 +696,10 @@ components:
properties:
type_of:
type: string
created_at:
description: Date the user became a follower
type: string
format: date-time
id:
description: Follow id
type: integer
@ -1552,12 +1557,14 @@ components:
value:
- type_of: user_follower
id: 12
created_at: "2021-04-02T04:21:46Z"
name: Mrs. Neda Morissette
path: "/nedamrsmorissette"
username: nedamrsmorissette
profile_image: https://res.cloudinary.com/...
- type_of: user_follower
id: 11
created_at: "2021-04-02T04:21:46Z"
name: Yoko Hintz
path: "/yokohintz"
username: yokohintz
@ -1567,6 +1574,7 @@ components:
value:
- type_of: listing
id: 1157
created_at: "2021-04-07 08:29:42 UTC"
title: TestBash Detroit
slug: testbash-detroit-50gb
body_markdown: "Do you want to learn about automation? Maybe you're interested in
@ -2166,7 +2174,7 @@ paths:
- $ref: '#/components/parameters/perPageParam30to1000'
responses:
"200":
description: A list of articles sorted by descending publish date
description: A list of articles sorted by descending publish date
content:
application/json:
schema:
@ -2709,6 +2717,17 @@ paths:
parameters:
- $ref: '#/components/parameters/pageParam'
- $ref: '#/components/parameters/perPageParam80to1000'
- in: query
name: sort
schema:
type: string
default: 'created_at'
pattern: '^-?\w+(,-?\w+)*$'
required: false
description: |
Specifies the sort order for the `created_at` param of the follow
relationship. To sort by newest followers first (descending order)
specify `?sort=-created_at`.
responses:
"200":
description: A list of followers

View file

@ -0,0 +1,42 @@
require "rails_helper"
RSpec.describe JsonApiSortParam do
let!(:controller) { Class.new { include JsonApiSortParam }.new }
it "returns a default sort order if there are no sorting params" do
params = controller.parse_sort_param(
"",
allowed_fields: [:created_at],
default_sort: { created_at: :desc },
)
expect(params).to eq({ created_at: :desc })
end
it "filters out non-allowed params", :aggregate_failures do
params = controller.parse_sort_param(
"-created_at,updated_at",
allowed_fields: [:updated_at],
default_sort: { created_at: :desc },
)
expect(params).not_to have_key(:created_at)
expect(params).to have_key(:updated_at)
end
it "generates a sort params hash" do
params = controller.parse_sort_param(
"created_at,-updated_at",
allowed_fields: %i[created_at updated_at],
default_sort: { created_at: :desc },
)
expect(params).to eq({ created_at: :asc, updated_at: :desc })
end
it "sorts the params in the correct order" do
params = controller.parse_sort_param(
"-updated_at,created_at",
allowed_fields: %i[created_at updated_at],
default_sort: { created_at: :desc },
)
expect(params.to_a).to eq([%i[created_at asc], %i[updated_at desc]])
end
end

View file

@ -43,6 +43,7 @@ RSpec.describe "Api::V0::FollowersController", type: :request do
expect(response_follower["path"]).to eq(follower.path)
expect(response_follower["username"]).to eq(follower.username)
expect(response_follower["profile_image"]).to eq(Images::Profile.call(follower.profile_image_url, length: 60))
expect(response_follower["created_at"]).to be_an_instance_of(String)
end
it "supports pagination" do
@ -58,12 +59,21 @@ RSpec.describe "Api::V0::FollowersController", type: :request do
expect(response.parsed_body.length).to eq(0)
end
it "order results for reverse following date" do
it "orders results by descending following date by default" do
follower2.follow(user)
follows = user.followings.order(id: :desc).last(2).map(&:id)
get api_followers_users_path, headers: headers
follows = user.followings.order(id: :desc).last(2).map(&:id)
result = response.parsed_body.map { |f| f["id"] }
expect(result).to eq(follows)
end
it "orders results by ascending following date if the 'sort' param is specified" do
follower2.follow(user)
follows = user.followings.order(id: :asc).last(2).map(&:id)
get api_followers_users_path, headers: headers, params: { sort: "created_at" }
result = response.parsed_body.map { |f| f["id"] }
expect(result).to eq(follows)
end