Skip to content

Commit

Permalink
Add generic pagination helper
Browse files Browse the repository at this point in the history
  • Loading branch information
MarcusGoldschmidt committed Dec 30, 2024
1 parent 0f7435f commit 7afe99b
Show file tree
Hide file tree
Showing 2 changed files with 221 additions and 0 deletions.
112 changes: 112 additions & 0 deletions pkg/pagination/generic_bag.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package pagination

import (
"encoding/json"
"fmt"
)

type genericSerializedPaginationBag[T any] struct {
States []T `json:"states"`
CurrentState *T `json:"current_state"`
}

// GenBag holds pagination states that can be serialized for use as page tokens. It acts as a stack that you can push and pop
// pagination operations from.
// This is a generic version of the Bag struct.
type GenBag[T any] struct {
states []T
currentState *T
}

func GenBagFromToken[T any](pToken Token) (*GenBag[T], error) {
bag := &GenBag[T]{}
err := bag.Unmarshal(pToken.Token)
if err != nil {
return nil, err
}
return bag, nil
}

func (pb *GenBag[T]) push(s T) {
if pb.currentState == nil {
pb.currentState = &s
return
}

pb.states = append(pb.states, *pb.currentState)
pb.currentState = &s
}

func (pb *GenBag[T]) pop() *T {
if pb.currentState == nil {
return nil
}

ret := *pb.currentState

if len(pb.states) > 0 {
pb.currentState = &pb.states[len(pb.states)-1]
pb.states = pb.states[:len(pb.states)-1]
} else {
pb.currentState = nil
}

return &ret
}

// Push pushes a new page state onto the stack.
func (pb *GenBag[T]) Push(state T) {
pb.push(state)
}

// Pop returns the current page action, and makes the top of the stack the current.
func (pb *GenBag[T]) Pop() *T {
return pb.pop()
}

// Current returns the current page state for the bag.
func (pb *GenBag[T]) Current() *T {
if pb.currentState == nil {
return nil
}

current := *pb.currentState
return &current
}

// Unmarshal takes an input string and unmarshals it onto the state object.
func (pb *GenBag[T]) Unmarshal(input string) error {
var target genericSerializedPaginationBag[T]

if input != "" {
err := json.Unmarshal([]byte(input), &target)
if err != nil {
return fmt.Errorf("page token corrupt: %w", err)
}

pb.states = target.States
pb.currentState = target.CurrentState
} else {
pb.states = nil
pb.currentState = nil
}

return nil
}

// Marshal returns a string encoding of the state object.
func (pb *GenBag[T]) Marshal() (string, error) {
if pb.currentState == nil {
return "", nil
}

data, err := json.Marshal(genericSerializedPaginationBag[T]{
States: pb.states,
CurrentState: pb.currentState,
})
if err != nil {
return "", err
}

return string(data), nil
}
109 changes: 109 additions & 0 deletions pkg/pagination/generic_bag_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package pagination

import (
"testing"

"github.com/stretchr/testify/require"
)

func TestGenBagMarshalling(t *testing.T) {
type innerStruct struct {
Age int
Name string
}

var bag GenBag[innerStruct]

bag.Push(innerStruct{Age: 10, Name: "John"})
bag.Push(innerStruct{Age: 20, Name: "Doe"})

// Marshal
marshalled, err := bag.Marshal()
require.NoError(t, err)

// Unmarshal
err = bag.Unmarshal(marshalled)
require.NoError(t, err)
}

func TestGenBagFromToken(t *testing.T) {
type innerStruct struct {
Age int
Name string
}

var bag GenBag[innerStruct]

bag.Push(innerStruct{Age: 10, Name: "John"})
bag.Push(innerStruct{Age: 20, Name: "Doe"})

// Marshal
marshalled, err := bag.Marshal()
require.NoError(t, err)

token := Token{Token: marshalled}

// Unmarshal
bagFromToken, err := GenBagFromToken[innerStruct](token)
require.NoError(t, err)

require.Equal(t, &bag, bagFromToken)
}

func TestGenBagUnmarshal(t *testing.T) {
type innerStruct struct {
Age int
Name string
}

var bag GenBag[innerStruct]

err := bag.Unmarshal("{invalid")
require.Error(t, err)

err = bag.Unmarshal("")

require.NoError(t, err)
require.Nil(t, bag.states)
require.Nil(t, bag.currentState)

marshal, err := bag.Marshal()
require.NoError(t, err)
require.Equal(t, "", marshal)

temp := bag.Current()
require.Nil(t, temp)

{
compare := innerStruct{Age: 10, Name: "John"}

bag.Push(compare)

current := bag.Current()
require.Equal(t, &compare, current)
}

{
first := innerStruct{Age: 10, Name: "John"}
second := innerStruct{Age: 15, Name: "Wick"}

bag.Push(first)
bag.Push(second)

current := bag.Current()
require.Equal(t, &second, current)
}

{
first := innerStruct{Age: 10, Name: "John"}
second := innerStruct{Age: 15, Name: "Wick"}

bag.Push(first)
bag.Push(second)
pop := bag.Pop()
require.Equal(t, pop, &second)

current := bag.Current()
require.Equal(t, &first, current)
}
}

0 comments on commit 7afe99b

Please sign in to comment.