From 7afe99b602068591b2f17e2b002a2f67aa70b86b Mon Sep 17 00:00:00 2001 From: Marcus Goldschmidt Date: Mon, 30 Dec 2024 18:17:23 -0400 Subject: [PATCH] Add generic pagination helper --- pkg/pagination/generic_bag.go | 112 +++++++++++++++++++++++++++++ pkg/pagination/generic_bag_test.go | 109 ++++++++++++++++++++++++++++ 2 files changed, 221 insertions(+) create mode 100644 pkg/pagination/generic_bag.go create mode 100644 pkg/pagination/generic_bag_test.go diff --git a/pkg/pagination/generic_bag.go b/pkg/pagination/generic_bag.go new file mode 100644 index 00000000..d4415dd4 --- /dev/null +++ b/pkg/pagination/generic_bag.go @@ -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 ¤t +} + +// 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 +} diff --git a/pkg/pagination/generic_bag_test.go b/pkg/pagination/generic_bag_test.go new file mode 100644 index 00000000..289f207e --- /dev/null +++ b/pkg/pagination/generic_bag_test.go @@ -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) + } +}