[bugfix] self-referencing collection pages for status replies (#2364)

This commit is contained in:
kim 2023-11-20 12:22:28 +00:00 committed by GitHub
commit 16275853eb
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
24 changed files with 611 additions and 427 deletions

View file

@ -131,3 +131,20 @@ func (b Boundary) Find(in []string) int {
}
return -1
}
// Boundary_FindFunc is functionally equivalent to Boundary{}.Find() but for an arbitrary type with ID.
// Note: this is not a Boundary{} method as Go generics are not supported in method receiver functions.
func Boundary_FindFunc[T any](b Boundary, in []T, get func(T) string) int { //nolint:revive
if get == nil {
panic("nil function")
}
if b.Value == "" {
return -1
}
for i := range in {
if get(in[i]) == b.Value {
return i
}
}
return -1
}

View file

@ -19,9 +19,8 @@ package paging
import (
"net/url"
"slices"
"strconv"
"golang.org/x/exp/slices"
)
type Page struct {
@ -117,7 +116,7 @@ func (p *Page) Page(in []string) []string {
// Output slice must
// ALWAYS be descending.
in = Reverse(in)
slices.Reverse(in)
}
} else {
// Default sort is descending,
@ -143,6 +142,66 @@ func (p *Page) Page(in []string) []string {
return in
}
// Page_PageFunc is functionally equivalent to Page{}.Page(), but for an arbitrary type with ID.
// Note: this is not a Page{} method as Go generics are not supported in method receiver functions.
func Page_PageFunc[WithID any](p *Page, in []WithID, get func(WithID) string) []WithID { //nolint:revive
if p == nil {
// no paging.
return in
}
if p.order().Ascending() {
// Sort type is ascending, input
// data is assumed to be ascending.
if minIdx := Boundary_FindFunc(p.Min, in, get); minIdx != -1 {
// Reslice skipping up to min.
in = in[minIdx+1:]
}
if maxIdx := Boundary_FindFunc(p.Max, in, get); maxIdx != -1 {
// Reslice stripping past max.
in = in[:maxIdx]
}
if p.Limit > 0 && p.Limit < len(in) {
// Reslice input to limit.
in = in[:p.Limit]
}
if len(in) > 1 {
// Clone input before
// any modifications.
in = slices.Clone(in)
// Output slice must
// ALWAYS be descending.
slices.Reverse(in)
}
} else {
// Default sort is descending,
// catching all cases when NOT
// ascending (even zero value).
if maxIdx := Boundary_FindFunc(p.Max, in, get); maxIdx != -1 {
// Reslice skipping up to max.
in = in[maxIdx+1:]
}
if minIdx := Boundary_FindFunc(p.Min, in, get); minIdx != -1 {
// Reslice stripping past min.
in = in[:minIdx]
}
if p.Limit > 0 && p.Limit < len(in) {
// Reslice input to limit.
in = in[:p.Limit]
}
}
return in
}
// Next creates a new instance for the next returnable page, using
// given max value. This preserves original limit and max key name.
func (p *Page) Next(lo, hi string) *Page {
@ -225,21 +284,24 @@ func (p *Page) ToLinkURL(proto, host, path string, queryParams url.Values) *url.
if queryParams == nil {
// Allocate new query parameters.
queryParams = make(url.Values)
} else {
// Before edit clone existing params.
queryParams = cloneQuery(queryParams)
}
if p.Min.Value != "" {
// A page-minimum query parameter is available.
queryParams.Add(p.Min.Name, p.Min.Value)
queryParams.Set(p.Min.Name, p.Min.Value)
}
if p.Max.Value != "" {
// A page-maximum query parameter is available.
queryParams.Add(p.Max.Name, p.Max.Value)
queryParams.Set(p.Max.Name, p.Max.Value)
}
if p.Limit > 0 {
// A page limit query parameter is available.
queryParams.Add("limit", strconv.Itoa(p.Limit))
queryParams.Set("limit", strconv.Itoa(p.Limit))
}
// Build URL string.
@ -250,3 +312,12 @@ func (p *Page) ToLinkURL(proto, host, path string, queryParams url.Values) *url.
RawQuery: queryParams.Encode(),
}
}
// cloneQuery clones input map of url values.
func cloneQuery(src url.Values) url.Values {
dst := make(url.Values, len(src))
for k, vs := range src {
dst[k] = slices.Clone(vs)
}
return dst
}

View file

@ -19,12 +19,12 @@ package paging_test
import (
"math/rand"
"slices"
"testing"
"time"
"github.com/oklog/ulid"
"github.com/superseriousbusiness/gotosocial/internal/paging"
"golang.org/x/exp/slices"
)
// random reader according to current-time source seed.
@ -77,9 +77,7 @@ func TestPage(t *testing.T) {
var cases = []Case{
CreateCase("minID and maxID set", func(ids []string) ([]string, *paging.Page, []string) {
// Ensure input slice sorted ascending for min_id
slices.SortFunc(ids, func(a, b string) bool {
return a > b // i.e. largest at lowest idx
})
slices.SortFunc(ids, ascending)
// Select random indices in slice.
minIdx := randRd.Intn(len(ids))
@ -93,7 +91,7 @@ var cases = []Case{
expect := slices.Clone(ids)
expect = cutLower(expect, minID)
expect = cutUpper(expect, maxID)
expect = paging.Reverse(expect)
slices.Reverse(expect)
// Return page and expected IDs.
return ids, &paging.Page{
@ -103,9 +101,7 @@ var cases = []Case{
}),
CreateCase("minID, maxID and limit set", func(ids []string) ([]string, *paging.Page, []string) {
// Ensure input slice sorted ascending for min_id
slices.SortFunc(ids, func(a, b string) bool {
return a > b // i.e. largest at lowest idx
})
slices.SortFunc(ids, ascending)
// Select random parameters in slice.
minIdx := randRd.Intn(len(ids))
@ -120,7 +116,7 @@ var cases = []Case{
expect := slices.Clone(ids)
expect = cutLower(expect, minID)
expect = cutUpper(expect, maxID)
expect = paging.Reverse(expect)
slices.Reverse(expect)
// Now limit the slice.
if limit < len(expect) {
@ -136,9 +132,7 @@ var cases = []Case{
}),
CreateCase("minID, maxID and too-large limit set", func(ids []string) ([]string, *paging.Page, []string) {
// Ensure input slice sorted ascending for min_id
slices.SortFunc(ids, func(a, b string) bool {
return a > b // i.e. largest at lowest idx
})
slices.SortFunc(ids, ascending)
// Select random parameters in slice.
minIdx := randRd.Intn(len(ids))
@ -152,7 +146,7 @@ var cases = []Case{
expect := slices.Clone(ids)
expect = cutLower(expect, minID)
expect = cutUpper(expect, maxID)
expect = paging.Reverse(expect)
slices.Reverse(expect)
// Return page and expected IDs.
return ids, &paging.Page{
@ -163,9 +157,7 @@ var cases = []Case{
}),
CreateCase("sinceID and maxID set", func(ids []string) ([]string, *paging.Page, []string) {
// Ensure input slice sorted descending for since_id
slices.SortFunc(ids, func(a, b string) bool {
return a < b // i.e. smallest at lowest idx
})
slices.SortFunc(ids, descending)
// Select random indices in slice.
sinceIdx := randRd.Intn(len(ids))
@ -188,9 +180,7 @@ var cases = []Case{
}),
CreateCase("maxID set", func(ids []string) ([]string, *paging.Page, []string) {
// Ensure input slice sorted descending for max_id
slices.SortFunc(ids, func(a, b string) bool {
return a < b // i.e. smallest at lowest idx
})
slices.SortFunc(ids, descending)
// Select random indices in slice.
maxIdx := randRd.Intn(len(ids))
@ -209,9 +199,7 @@ var cases = []Case{
}),
CreateCase("sinceID set", func(ids []string) ([]string, *paging.Page, []string) {
// Ensure input slice sorted descending for since_id
slices.SortFunc(ids, func(a, b string) bool {
return a < b
})
slices.SortFunc(ids, descending)
// Select random indices in slice.
sinceIdx := randRd.Intn(len(ids))
@ -230,9 +218,7 @@ var cases = []Case{
}),
CreateCase("minID set", func(ids []string) ([]string, *paging.Page, []string) {
// Ensure input slice sorted ascending for min_id
slices.SortFunc(ids, func(a, b string) bool {
return a > b // i.e. largest at lowest idx
})
slices.SortFunc(ids, ascending)
// Select random indices in slice.
minIdx := randRd.Intn(len(ids))
@ -243,7 +229,7 @@ var cases = []Case{
// Create expected output.
expect := slices.Clone(ids)
expect = cutLower(expect, minID)
expect = paging.Reverse(expect)
slices.Reverse(expect)
// Return page and expected IDs.
return ids, &paging.Page{
@ -296,3 +282,21 @@ func generateSlice(len int) []string {
}
return in
}
func ascending(a, b string) int {
if a > b {
return 1
} else if a < b {
return -1
}
return 0
}
func descending(a, b string) int {
if a < b {
return 1
} else if a > b {
return -1
}
return 0
}

View file

@ -1,43 +0,0 @@
// GoToSocial
// Copyright (C) GoToSocial Authors admin@gotosocial.org
// SPDX-License-Identifier: AGPL-3.0-or-later
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
package paging
// Reverse will reverse the given input slice.
func Reverse(in []string) []string {
var (
// Start at front.
i = 0
// Start at back.
j = len(in) - 1
)
for i < j {
// Swap i,j index values in slice.
in[i], in[j] = in[j], in[i]
// incr + decr,
// looping until
// they meet in
// the middle.
i++
j--
}
return in
}