Skip to content

Commit

Permalink
Merge pull request #66 from sashahilton00/master
Browse files Browse the repository at this point in the history
feat: add support for custom encoder/decoder implementations
  • Loading branch information
pilagod authored Oct 16, 2024
2 parents 21e1004 + d766606 commit d0a99c8
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 20 deletions.
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

0 comments on commit d0a99c8

Please sign in to comment.