Skip to content

Commit

Permalink
Merge pull request #289 from JinHuangAtZen/jin.huang/generic-iterator
Browse files Browse the repository at this point in the history
Make pagination iterator generic
  • Loading branch information
nukosuke authored Oct 4, 2023
2 parents 096ccfd + 72dc8ff commit 762f6d1
Show file tree
Hide file tree
Showing 13 changed files with 1,164 additions and 634 deletions.
1 change: 0 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ jobs:
strategy:
matrix:
go-version:
- 1.17.x
- 1.18.x
- 1.19.x
- 1.20.x
Expand Down
10 changes: 9 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@ module github.com/nukosuke/go-zendesk
go 1.19

require (
github.com/golang/mock v1.6.0
github.com/google/go-querystring v1.1.0
go.uber.org/mock v0.3.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/stretchr/testify v1.8.4 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
39 changes: 13 additions & 26 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,30 +1,17 @@
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
2 changes: 1 addition & 1 deletion zendesk/api.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package zendesk

//nolint
//go:generate mockgen -destination=mock/client.go -package=mock -mock_names=API=Client github.com/nukosuke/go-zendesk/zendesk API
//go:generate mockgen -source=api.go -destination=mock/client.go -package=mock -mock_names=API=Client github.com/nukosuke/go-zendesk/zendesk API

// API an interface containing all of the zendesk client methods
type API interface {
Expand Down
69 changes: 69 additions & 0 deletions zendesk/group.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ type GroupListOptions struct {
// GroupAPI an interface containing all methods associated with zendesk groups
type GroupAPI interface {
GetGroups(ctx context.Context, opts *GroupListOptions) ([]Group, Page, error)
GetGroupsOBP(ctx context.Context, opts *OBPOptions) ([]Group, Page, error)
GetGroupsCBP(ctx context.Context, opts *CBPOptions) ([]Group, CursorPaginationMeta, error)
GetGroupsIterator(ctx context.Context, opts *PaginationOptions) *Iterator[Group]
GetGroup(ctx context.Context, groupID int64) (Group, error)
CreateGroup(ctx context.Context, group Group) (Group, error)
UpdateGroup(ctx context.Context, groupID int64, group Group) (Group, error)
Expand Down Expand Up @@ -66,6 +69,72 @@ func (z *Client) GetGroups(ctx context.Context, opts *GroupListOptions) ([]Group
return data.Groups, data.Page, nil
}

// GetGroupsOBP fetches group list from OBP (Offset Based Pagination)
// https://developer.zendesk.com/rest_api/docs/support/groups#list-groups
func (z *Client) GetGroupsOBP(ctx context.Context, opts *OBPOptions) ([]Group, Page, error) {
var data struct {
Groups []Group `json:"groups"`
Page
}

tmp := opts
if tmp == nil {
tmp = &OBPOptions{}
}

u, err := addOptions("/groups.json", tmp)
if err != nil {
return []Group{}, Page{}, err
}

err = getData(z, ctx, u, &data)
if err != nil {
return []Group{}, Page{}, err
}
return data.Groups, data.Page, nil
}

// GetGroupsIterator returns an Iterator to iterate over groups
//
// ref: https://developer.zendesk.com/rest_api/docs/support/groups#list-groups
func (z *Client) GetGroupsIterator(ctx context.Context, opts *PaginationOptions) *Iterator[Group] {
return &Iterator[Group]{
pageSize: opts.PageSize,
hasMore: true,
isCBP: opts.IsCBP,
pageAfter: "",
pageIndex: 1,
ctx: ctx,
obpFunc: z.GetGroupsOBP,
cbpFunc: z.GetGroupsCBP,
}
}

// GetGroupsCBP fetches group list from CBP (Cursor Based Pagination)
// https://developer.zendesk.com/rest_api/docs/support/groups#list-groups
func (z *Client) GetGroupsCBP(ctx context.Context, opts *CBPOptions) ([]Group, CursorPaginationMeta, error) {
var data struct {
Groups []Group `json:"groups"`
Meta CursorPaginationMeta `json:"meta"`
}

tmp := opts
if tmp == nil {
tmp = &CBPOptions{}
}

u, err := addOptions("/groups.json", tmp)
if err != nil {
return []Group{}, data.Meta, err
}

err = getData(z, ctx, u, &data)
if err != nil {
return []Group{}, data.Meta, err
}
return data.Groups, data.Meta, nil
}

// CreateGroup creates new group
// https://developer.zendesk.com/rest_api/docs/support/groups#create-group
func (z *Client) CreateGroup(ctx context.Context, group Group) (Group, error) {
Expand Down
57 changes: 57 additions & 0 deletions zendesk/group_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,33 @@ import (
"testing"
)

func TestGetGroupsIterator(t *testing.T) {
mockAPI := newMockAPI(http.MethodGet, "groups.json")
client := newTestClient(mockAPI)
defer mockAPI.Close()

ops := NewPaginationOptions()
ops.PageSize = 10

it := client.GetGroupsIterator(ctx, ops)

expectedLength := 1
groupsCount := 0
for it.HasMore() {
groups, err := it.GetNext()
if len(groups) != expectedLength {
t.Fatalf("expected length of groups is 1, but got %d", len(groups))
}
groupsCount += len(groups)
if err != nil {
t.Fatalf("Failed to get groups: %s", err)
}
}
if groupsCount != 1 {
t.Fatalf("expected length of groups is 1, but got %d", groupsCount)
}
}

func TestGetGroups(t *testing.T) {
mockAPI := newMockAPI(http.MethodGet, "groups.json")
client := newTestClient(mockAPI)
Expand All @@ -21,6 +48,36 @@ func TestGetGroups(t *testing.T) {
}
}

func TestGetGroupsOBP(t *testing.T) {
mockAPI := newMockAPI(http.MethodGet, "groups.json")
client := newTestClient(mockAPI)
defer mockAPI.Close()

groups, _, err := client.GetGroupsOBP(ctx, nil)
if err != nil {
t.Fatalf("Failed to get groups: %s", err)
}

if len(groups) != 1 {
t.Fatalf("expected length of groups is 1, but got %d", len(groups))
}
}

func TestGetGroupsCBP(t *testing.T) {
mockAPI := newMockAPI(http.MethodGet, "groups.json")
client := newTestClient(mockAPI)
defer mockAPI.Close()

groups, _, err := client.GetGroupsCBP(ctx, nil)
if err != nil {
t.Fatalf("Failed to get groups: %s", err)
}

if len(groups) != 1 {
t.Fatalf("expected length of groups is 1, but got %d", len(groups))
}
}

func TestCreateGroup(t *testing.T) {
mockAPI := newMockAPIWithStatus(http.MethodPost, "groups.json", http.StatusCreated)
client := newTestClient(mockAPI)
Expand Down
124 changes: 124 additions & 0 deletions zendesk/iterator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package zendesk

import (
"context"
)

// PaginationOptions struct represents general pagination options.
// PageSize specifies the number of items per page, IsCBP indicates if it's cursor-based pagination,
// SortBy and SortOrder describe how to sort the items in Offset Based Pagination, and Sort describes how to sort items in Cursor Based Pagination.
type PaginationOptions struct {
CommonOptions
PageSize int //default is 100
IsCBP bool //default is true
}

// NewPaginationOptions() returns a pointer to a new PaginationOptions struct with default values (PageSize is 100, IsCBP is true).
func NewPaginationOptions() *PaginationOptions {
return &PaginationOptions{
PageSize: 100,
IsCBP: true,
}
}

type CommonOptions struct {
Active bool `url:"active,omitempty"`
Role string `url:"role,omitempty"`
Roles []string `url:"role[],omitempty"`
PermissionSet int64 `url:"permission_set,omitempty"`

// SortBy can take "assignee", "assignee.name", "created_at", "group", "id",
// "locale", "requester", "requester.name", "status", "subject", "updated_at"
SortBy string `url:"sort_by,omitempty"`

// SortOrder can take "asc" or "desc"
SortOrder string `url:"sort_order,omitempty"`
Sort string `url:"sort,omitempty"`
Id int64
}

// CBPOptions struct is used to specify options for listing objects in CBP (Cursor Based Pagination).
// It embeds the CursorPagination struct for pagination and provides an option Sort for sorting the result.
type CBPOptions struct {
CursorPagination
CommonOptions
}

// OBPOptions struct is used to specify options for listing objects in OBP (Offset Based Pagination).
// It embeds the PageOptions struct for pagination and provides options for sorting the result;
// SortBy specifies the field to sort by, and SortOrder specifies the order (either 'asc' or 'desc').
type OBPOptions struct {
PageOptions
CommonOptions
}

// ObpFunc defines the signature of the function used to list objects in OBP.
type ObpFunc[T any] func(ctx context.Context, opts *OBPOptions) ([]T, Page, error)

// CbpFunc defines the signature of the function used to list objects in CBP.
type CbpFunc[T any] func(ctx context.Context, opts *CBPOptions) ([]T, CursorPaginationMeta, error)

// terator struct provides a convenient and genric way to iterate over pages of objects in either OBP or CBP.
// It holds state for iteration, including the current page size, a flag indicating more pages, pagination type (OBP or CBP), and sorting options.
type Iterator[T any] struct {
CommonOptions
// generic fields
pageSize int
hasMore bool
isCBP bool

// OBP fields
pageIndex int

// CBP fields
pageAfter string

// common fields
ctx context.Context
obpFunc ObpFunc[T]
cbpFunc CbpFunc[T]
}

// HasMore() returns a boolean indicating whether more pages are available for iteration.
func (i *Iterator[T]) HasMore() bool {
return i.hasMore
}

// GetNext() retrieves the next batch of objects according to the current pagination and sorting options.
// It updates the state of the iterator for subsequent calls.
// In case of an error, it sets hasMore to false and returns an error.
func (i *Iterator[T]) GetNext() ([]T, error) {
if !i.isCBP {
obpOps := &OBPOptions{
PageOptions: PageOptions{
PerPage: i.pageSize,
Page: i.pageIndex,
},
CommonOptions: i.CommonOptions,
}
results, page, err := i.obpFunc(i.ctx, obpOps)
if err != nil {
i.hasMore = false
return nil, err
}
i.hasMore = page.HasNext()
i.pageIndex++
return results, nil
}

cbpOps := &CBPOptions{
CursorPagination: CursorPagination{
PageSize: i.pageSize,
PageAfter: i.pageAfter,
},
CommonOptions: i.CommonOptions,
}
results, meta, err := i.cbpFunc(i.ctx, cbpOps)
if err != nil {
i.hasMore = false
return nil, err
}
i.hasMore = meta.HasMore
i.pageAfter = meta.AfterCursor
return results, nil
}
Loading

0 comments on commit 762f6d1

Please sign in to comment.