Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add support for custom encoder/decoder implementations #66

Merged
merged 4 commits into from
Oct 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 66 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ A paginator doing cursor-based pagination based on [GORM](https://github.com/go-
- GORM `column` tag supported.
- Error handling enhancement.
- Exporting `cursor` module for advanced usage.
- Implement custom codec for cursor encoding/decoding.

## Installation

Expand Down Expand Up @@ -131,6 +132,71 @@ We first need to create a `paginator.Paginator` for `User`, here are some useful
}
```

4. By default the library encodes cursors with `base64`. If a custom encoding/decoding implementation is required, this can be implemented and passed as part of the configuration:


First implement your custom codec such that it conforms to the `CursorCodec` interface:


```go
type CursorCodec interface {
// Encode encodes model fields into cursor
Encode(
fields []pc.EncoderField,
model interface{},
) (string, error)

// Decode decodes cursor into model fields
Decode(
fields []pc.DecoderField,
cursor string,
model interface{},
) ([]interface{}, error)
}

type customCodec struct {}

func (cc *CustomCodec) Encode(fields []pc.EncoderField, model interface{}) (string, error) {
...
}

func (cc *CustomCodec) Decode(fields []pc.DecoderField, cursor string, model interface{}) ([]interface{}, error) {
...
}
```

Then pass an instance of your codec during initialisation:

```go
func CreateUserPaginator(/* ... */) {
codec := &customCodec{}

p := paginator.New(
&paginator.Config{
Rules: []paginator.Rule{
{
Key: "ID",
},
{
Key: "JoinedAt",
Order: paginator.DESC,
SQLRepr: "users.created_at",
NULLReplacement: "1970-01-01",
},
},
Limit: 10,
// supply a custom implementation for the encoder/decoder
CursorCodec: codec,
// Order here will apply to keys without order specified.
// In this example paginator will order by "ID" ASC, "JoinedAt" DESC.
Order: paginator.ASC,
},
)
// ...
return p
}
```

After knowing how to setup the paginator, we can start paginating `User` with GORM:

```go
Expand Down
42 changes: 40 additions & 2 deletions paginator/cursor.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,44 @@
package paginator

import "github.com/pilagod/gorm-cursor-paginator/v2/cursor"
import (
pc "github.com/pilagod/gorm-cursor-paginator/v2/cursor"
)

// Cursor re-exports cursor.Cursor
type Cursor = cursor.Cursor
type Cursor = pc.Cursor

// CursorCodec encodes/decodes cursor
type CursorCodec interface {
// Encode encodes model fields into cursor
Encode(
fields []pc.EncoderField,
model interface{},
) (string, error)

// Decode decodes cursor into model fields
Decode(
fields []pc.DecoderField,
cursor string,
model interface{},
) ([]interface{}, error)
}

// JSONCursorCodec encodes/decodes cursor in JSON format
type JSONCursorCodec struct{}

// Encode encodes model fields into JSON format cursor
func (*JSONCursorCodec) Encode(
fields []pc.EncoderField,
model interface{},
) (string, error) {
return pc.NewEncoder(fields).Encode(model)
}

// Decode decodes JSON format cursor into model fields
func (*JSONCursorCodec) Decode(
fields []pc.DecoderField,
cursor string,
model interface{},
) ([]interface{}, error) {
return pc.NewDecoder(fields).Decode(cursor, model)
}
12 changes: 12 additions & 0 deletions paginator/option.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ var defaultConfig = Config{
Limit: 10,
Order: DESC,
AllowTupleCmp: FALSE,
CursorCodec: &JSONCursorCodec{},
}

// Option for paginator
Expand All @@ -28,6 +29,7 @@ type Config struct {
After string
Before string
AllowTupleCmp Flag
CursorCodec CursorCodec
}

// Apply applies config to paginator
Expand All @@ -54,6 +56,9 @@ func (c *Config) Apply(p *Paginator) {
if c.AllowTupleCmp != "" {
p.SetAllowTupleCmp(c.AllowTupleCmp == TRUE)
}
if c.CursorCodec != nil {
p.SetCursorCodec(c.CursorCodec)
}
}

// WithRules configures rules for paginator
Expand Down Expand Up @@ -104,3 +109,10 @@ func WithAllowTupleCmp(flag Flag) Option {
AllowTupleCmp: flag,
}
}

// WithCursorCodec configures custom cursor codec
func WithCursorCodec(codec CursorCodec) Option {
return &Config{
CursorCodec: codec,
}
}
27 changes: 10 additions & 17 deletions paginator/paginator.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ type Paginator struct {
limit int
order Order
allowTupleCmp bool
cursorCodec CursorCodec
}

// SetRules sets paging rules
Expand Down Expand Up @@ -71,6 +72,11 @@ func (p *Paginator) SetAllowTupleCmp(allow bool) {
p.allowTupleCmp = allow
}

// SetCursorCodec sets custom cursor codec
func (p *Paginator) SetCursorCodec(codec CursorCodec) {
p.cursorCodec = codec
}

// Paginate paginates data
func (p *Paginator) Paginate(db *gorm.DB, dest interface{}) (result *gorm.DB, c Cursor, err error) {
if err = p.validate(db, dest); err != nil {
Expand Down Expand Up @@ -104,16 +110,6 @@ func (p *Paginator) Paginate(db *gorm.DB, dest interface{}) (result *gorm.DB, c
return
}

// GetCursorEncoder returns cursor encoder based on paginator rules
func (p *Paginator) GetCursorEncoder() *cursor.Encoder {
return cursor.NewEncoder(p.getEncoderFields())
}

// GetCursorDecoder returns cursor decoder based on paginator rules
func (p *Paginator) GetCursorDecoder() *cursor.Decoder {
return cursor.NewDecoder(p.getDecoderFields())
}

/* private */

func (p *Paginator) validate(db *gorm.DB, dest interface{}) (err error) {
Expand Down Expand Up @@ -189,14 +185,12 @@ func isNil(i interface{}) bool {
}

func (p *Paginator) decodeCursor(dest interface{}) (result []interface{}, err error) {
decoder := p.GetCursorDecoder()

if p.isForward() {
if result, err = decoder.Decode(*p.cursor.After, dest); err != nil {
if result, err = p.cursorCodec.Decode(p.getDecoderFields(), *p.cursor.After, dest); err != nil {
err = ErrInvalidCursor
}
} else if p.isBackward() {
if result, err = decoder.Decode(*p.cursor.Before, dest); err != nil {
if result, err = p.cursorCodec.Decode(p.getDecoderFields(), *p.cursor.Before, dest); err != nil {
err = ErrInvalidCursor
}
}
Expand Down Expand Up @@ -312,18 +306,17 @@ func (p *Paginator) buildCursorSQLQueryArgs(fields []interface{}) (args []interf
}

func (p *Paginator) encodeCursor(elems reflect.Value, hasMore bool) (result Cursor, err error) {
encoder := p.GetCursorEncoder()
// encode after cursor
if p.isBackward() || hasMore {
c, err := encoder.Encode(elems.Index(elems.Len() - 1))
c, err := p.cursorCodec.Encode(p.getEncoderFields(), elems.Index(elems.Len()-1))
if err != nil {
return Cursor{}, err
}
result.After = &c
}
// encode before cursor
if p.isForward() || (hasMore && p.isBackward()) {
c, err := encoder.Encode(elems.Index(0))
c, err := p.cursorCodec.Encode(p.getEncoderFields(), elems.Index(0))
if err != nil {
return Cursor{}, err
}
Expand Down
82 changes: 81 additions & 1 deletion paginator/paginator_paginate_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
package paginator

import (
"fmt"
"reflect"
"strconv"
"time"

"github.com/pilagod/pointer"
"gorm.io/gorm"

pc "github.com/pilagod/gorm-cursor-paginator/v2/cursor"
"github.com/pilagod/gorm-cursor-paginator/v2/internal/util"
"github.com/pilagod/pointer"
)

func (s *paginatorSuite) TestPaginateDefaultOptions() {
Expand Down Expand Up @@ -644,6 +649,8 @@ func (s *paginatorSuite) TestPaginateReplaceNULL() {
s.assertForwardOnly(c)
}

/* Custom Type */

func (s *paginatorSuite) TestPaginateCustomTypeInt() {
s.givenOrders(9)

Expand Down Expand Up @@ -827,6 +834,79 @@ func (s *paginatorSuite) TestPaginateCustomTypeNullable() {
s.assertIDs(p5, 1)
}

/* Custom Cursor Codec */

type idCursorCodec struct{}

func (c *idCursorCodec) Encode(fields []pc.EncoderField, model interface{}) (string, error) {
if len(fields) != 1 || fields[0].Key != "ID" {
return "", fmt.Errorf("ID field is required")
}
id := util.ReflectValue(model).FieldByName("ID").Interface()
return fmt.Sprintf("%d", id), nil
}

func (c *idCursorCodec) Decode(fields []pc.DecoderField, cursor string, model interface{}) ([]interface{}, error) {
if len(fields) != 1 || fields[0].Key != "ID" {
return nil, fmt.Errorf("ID field is required")
}
if _, ok := util.ReflectType(model).FieldByName("ID"); !ok {
return nil, fmt.Errorf("ID field is required on model")
}
id, err := strconv.Atoi(cursor)
if err != nil {
return nil, err
}
return []interface{}{id}, nil
}

func (s *paginatorSuite) TestPaginateCustomCodec() {
s.givenOrders([]order{
{
ID: 1,
},
{
ID: 2,
},
{
ID: 3,
},
})

cfg := Config{
Limit: 2,
}
codec := &idCursorCodec{}

var p1 []order
_, c, _ := New(
&cfg,
WithCursorCodec(codec),
).Paginate(s.db, &p1)
s.Len(p1, 2)
s.assertForwardOnly(c)
s.assertIDs(p1, 3, 2)

var p2 []order
_, c, _ = New(
&cfg,
WithCursorCodec(codec),
WithAfter(*c.After),
).Paginate(s.db, &p2)
s.Len(p2, 1)
s.assertBackwardOnly(c)
s.assertIDs(p2, 1)

var p3 []order
_, c, _ = New(
&cfg,
WithCursorCodec(codec),
WithBefore(*c.Before),
).Paginate(s.db, &p3)
s.Len(p3, 2)
s.assertIDs(p3, 3, 2)
}

/* compatibility */

func (s *paginatorSuite) TestPaginateConsistencyBetweenBuilderAndKeyOptions() {
Expand Down
Loading