summaryrefslogtreecommitdiff
path: root/internal/util
diff options
context:
space:
mode:
authorLibravatar tobi <31960611+tsmethurst@users.noreply.github.com>2023-05-22 16:32:36 +0200
committerLibravatar GitHub <noreply@github.com>2023-05-22 16:32:36 +0200
commitc48abd8bc0e708fedcddd7985c00243580464452 (patch)
tree6f9eb8b0537c75c3d68f1a9e0135870e6e0e7575 /internal/util
parent[chore]: Bump github.com/go-playground/validator/v10 (#1812) (diff)
downloadgotosocial-c48abd8bc0e708fedcddd7985c00243580464452.tar.xz
[chore] update account statuses paging logic (#1814)
Diffstat (limited to 'internal/util')
-rw-r--r--internal/util/paging.go108
-rw-r--r--internal/util/paging_test.go139
2 files changed, 203 insertions, 44 deletions
diff --git a/internal/util/paging.go b/internal/util/paging.go
index 3d620ca1f..0ab4b9567 100644
--- a/internal/util/paging.go
+++ b/internal/util/paging.go
@@ -20,6 +20,7 @@ package util
import (
"fmt"
"net/url"
+ "strings"
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
@@ -47,6 +48,13 @@ type PageableResponseParams struct {
// a bunch of pageable items (notifications, statuses, etc), as well
// as a Link header to inform callers of where to find next/prev items.
func PackagePageableResponse(params PageableResponseParams) (*apimodel.PageableResponse, gtserror.WithCode) {
+ if len(params.Items) == 0 {
+ // No items to page through.
+ return EmptyPageableResponse(), nil
+ }
+
+ // Set default paging values, if
+ // they weren't set by the caller.
if params.NextMaxIDKey == "" {
params.NextMaxIDKey = "max_id"
}
@@ -55,58 +63,70 @@ func PackagePageableResponse(params PageableResponseParams) (*apimodel.PageableR
params.PrevMinIDKey = "min_id"
}
- pageableResponse := EmptyPageableResponse()
+ var (
+ protocol = config.GetProtocol()
+ host = config.GetHost()
+ nextLink string
+ prevLink string
+ linkHeaderParts = make([]string, 0, 2)
+ )
- if len(params.Items) == 0 {
- return pageableResponse, nil
- }
+ // Parse next link.
+ if params.NextMaxIDValue != "" {
+ nextRaw := params.NextMaxIDKey + "=" + params.NextMaxIDValue
- // items
- pageableResponse.Items = params.Items
+ if params.Limit != 0 {
+ nextRaw = fmt.Sprintf("limit=%d&", params.Limit) + nextRaw
+ }
- protocol := config.GetProtocol()
- host := config.GetHost()
+ for _, p := range params.ExtraQueryParams {
+ nextRaw += "&" + p
+ }
- // next
- nextRaw := params.NextMaxIDKey + "=" + params.NextMaxIDValue
- if params.Limit != 0 {
- nextRaw = fmt.Sprintf("limit=%d&", params.Limit) + nextRaw
- }
- for _, p := range params.ExtraQueryParams {
- nextRaw = nextRaw + "&" + p
- }
- nextLink := &url.URL{
- Scheme: protocol,
- Host: host,
- Path: params.Path,
- RawQuery: nextRaw,
- }
- nextLinkString := nextLink.String()
- pageableResponse.NextLink = nextLinkString
+ nextLink = func() string {
+ u := &url.URL{
+ Scheme: protocol,
+ Host: host,
+ Path: params.Path,
+ RawQuery: nextRaw,
+ }
+ return u.String()
+ }()
- // prev
- prevRaw := params.PrevMinIDKey + "=" + params.PrevMinIDValue
- if params.Limit != 0 {
- prevRaw = fmt.Sprintf("limit=%d&", params.Limit) + prevRaw
+ linkHeaderParts = append(linkHeaderParts, `<`+nextLink+`>; rel="next"`)
}
- for _, p := range params.ExtraQueryParams {
- prevRaw = prevRaw + "&" + p
- }
- prevLink := &url.URL{
- Scheme: protocol,
- Host: host,
- Path: params.Path,
- RawQuery: prevRaw,
- }
- prevLinkString := prevLink.String()
- pageableResponse.PrevLink = prevLinkString
- // link header
- next := fmt.Sprintf("<%s>; rel=\"next\"", nextLinkString)
- prev := fmt.Sprintf("<%s>; rel=\"prev\"", prevLinkString)
- pageableResponse.LinkHeader = next + ", " + prev
+ // Parse prev link.
+ if params.PrevMinIDValue != "" {
+ prevRaw := params.PrevMinIDKey + "=" + params.PrevMinIDValue
+
+ if params.Limit != 0 {
+ prevRaw = fmt.Sprintf("limit=%d&", params.Limit) + prevRaw
+ }
+
+ for _, p := range params.ExtraQueryParams {
+ prevRaw = prevRaw + "&" + p
+ }
- return pageableResponse, nil
+ prevLink = func() string {
+ u := &url.URL{
+ Scheme: protocol,
+ Host: host,
+ Path: params.Path,
+ RawQuery: prevRaw,
+ }
+ return u.String()
+ }()
+
+ linkHeaderParts = append(linkHeaderParts, `<`+prevLink+`>; rel="prev"`)
+ }
+
+ return &apimodel.PageableResponse{
+ Items: params.Items,
+ LinkHeader: strings.Join(linkHeaderParts, ", "),
+ NextLink: nextLink,
+ PrevLink: prevLink,
+ }, nil
}
// EmptyPageableResponse just returns an empty
diff --git a/internal/util/paging_test.go b/internal/util/paging_test.go
new file mode 100644
index 000000000..685db14ba
--- /dev/null
+++ b/internal/util/paging_test.go
@@ -0,0 +1,139 @@
+// 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 util_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+type PagingSuite struct {
+ suite.Suite
+}
+
+func (suite *PagingSuite) TestPagingStandard() {
+ config.SetHost("example.org")
+
+ params := util.PageableResponseParams{
+ Items: make([]interface{}, 10, 10),
+ Path: "/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses",
+ NextMaxIDValue: "01H11KA1DM2VH3747YDE7FV5HN",
+ PrevMinIDValue: "01H11KBBVRRDYYC5KEPME1NP5R",
+ Limit: 10,
+ }
+
+ resp, errWithCode := util.PackagePageableResponse(params)
+ if errWithCode != nil {
+ suite.FailNow(errWithCode.Error())
+ }
+
+ suite.Equal(make([]interface{}, 10, 10), resp.Items)
+ suite.Equal(`<https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?limit=10&max_id=01H11KA1DM2VH3747YDE7FV5HN>; rel="next", <https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?limit=10&min_id=01H11KBBVRRDYYC5KEPME1NP5R>; rel="prev"`, resp.LinkHeader)
+ suite.Equal(`https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?limit=10&max_id=01H11KA1DM2VH3747YDE7FV5HN`, resp.NextLink)
+ suite.Equal(`https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?limit=10&min_id=01H11KBBVRRDYYC5KEPME1NP5R`, resp.PrevLink)
+}
+
+func (suite *PagingSuite) TestPagingNoLimit() {
+ config.SetHost("example.org")
+
+ params := util.PageableResponseParams{
+ Items: make([]interface{}, 10, 10),
+ Path: "/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses",
+ NextMaxIDValue: "01H11KA1DM2VH3747YDE7FV5HN",
+ PrevMinIDValue: "01H11KBBVRRDYYC5KEPME1NP5R",
+ }
+
+ resp, errWithCode := util.PackagePageableResponse(params)
+ if errWithCode != nil {
+ suite.FailNow(errWithCode.Error())
+ }
+
+ suite.Equal(make([]interface{}, 10, 10), resp.Items)
+ suite.Equal(`<https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?max_id=01H11KA1DM2VH3747YDE7FV5HN>; rel="next", <https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?min_id=01H11KBBVRRDYYC5KEPME1NP5R>; rel="prev"`, resp.LinkHeader)
+ suite.Equal(`https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?max_id=01H11KA1DM2VH3747YDE7FV5HN`, resp.NextLink)
+ suite.Equal(`https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?min_id=01H11KBBVRRDYYC5KEPME1NP5R`, resp.PrevLink)
+}
+
+func (suite *PagingSuite) TestPagingNoNextID() {
+ config.SetHost("example.org")
+
+ params := util.PageableResponseParams{
+ Items: make([]interface{}, 10, 10),
+ Path: "/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses",
+ PrevMinIDValue: "01H11KBBVRRDYYC5KEPME1NP5R",
+ Limit: 10,
+ }
+
+ resp, errWithCode := util.PackagePageableResponse(params)
+ if errWithCode != nil {
+ suite.FailNow(errWithCode.Error())
+ }
+
+ suite.Equal(make([]interface{}, 10, 10), resp.Items)
+ suite.Equal(`<https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?limit=10&min_id=01H11KBBVRRDYYC5KEPME1NP5R>; rel="prev"`, resp.LinkHeader)
+ suite.Equal(``, resp.NextLink)
+ suite.Equal(`https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?limit=10&min_id=01H11KBBVRRDYYC5KEPME1NP5R`, resp.PrevLink)
+}
+
+func (suite *PagingSuite) TestPagingNoPrevID() {
+ config.SetHost("example.org")
+
+ params := util.PageableResponseParams{
+ Items: make([]interface{}, 10, 10),
+ Path: "/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses",
+ NextMaxIDValue: "01H11KA1DM2VH3747YDE7FV5HN",
+ Limit: 10,
+ }
+
+ resp, errWithCode := util.PackagePageableResponse(params)
+ if errWithCode != nil {
+ suite.FailNow(errWithCode.Error())
+ }
+
+ suite.Equal(make([]interface{}, 10, 10), resp.Items)
+ suite.Equal(`<https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?limit=10&max_id=01H11KA1DM2VH3747YDE7FV5HN>; rel="next"`, resp.LinkHeader)
+ suite.Equal(`https://example.org/api/v1/accounts/01H11KA68PM4NNYJEG0FJQ90R3/statuses?limit=10&max_id=01H11KA1DM2VH3747YDE7FV5HN`, resp.NextLink)
+ suite.Equal(``, resp.PrevLink)
+}
+
+func (suite *PagingSuite) TestPagingNoItems() {
+ config.SetHost("example.org")
+
+ params := util.PageableResponseParams{
+ NextMaxIDValue: "01H11KA1DM2VH3747YDE7FV5HN",
+ PrevMinIDValue: "01H11KBBVRRDYYC5KEPME1NP5R",
+ Limit: 10,
+ }
+
+ resp, errWithCode := util.PackagePageableResponse(params)
+ if errWithCode != nil {
+ suite.FailNow(errWithCode.Error())
+ }
+
+ suite.Empty(resp.Items)
+ suite.Empty(resp.LinkHeader)
+ suite.Empty(resp.NextLink)
+ suite.Empty(resp.PrevLink)
+}
+
+func TestPagingSuite(t *testing.T) {
+ suite.Run(t, &PagingSuite{})
+}