Skip to content

Commit 762f6d1

Browse files
author
nukosuke
authored
Merge pull request #289 from JinHuangAtZen/jin.huang/generic-iterator
Make pagination iterator generic
2 parents 096ccfd + 72dc8ff commit 762f6d1

File tree

13 files changed

+1164
-634
lines changed

13 files changed

+1164
-634
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ jobs:
77
strategy:
88
matrix:
99
go-version:
10-
- 1.17.x
1110
- 1.18.x
1211
- 1.19.x
1312
- 1.20.x

go.mod

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,14 @@ module github.com/nukosuke/go-zendesk
33
go 1.19
44

55
require (
6-
github.com/golang/mock v1.6.0
76
github.com/google/go-querystring v1.1.0
7+
go.uber.org/mock v0.3.0
8+
)
9+
10+
require (
11+
github.com/davecgh/go-spew v1.1.1 // indirect
12+
github.com/google/go-cmp v0.5.9 // indirect
13+
github.com/pmezard/go-difflib v1.0.0 // indirect
14+
github.com/stretchr/testify v1.8.4 // indirect
15+
gopkg.in/yaml.v3 v3.0.1 // indirect
816
)

go.sum

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

zendesk/api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
package zendesk
22

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

66
// API an interface containing all of the zendesk client methods
77
type API interface {

zendesk/group.go

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,9 @@ type GroupListOptions struct {
3030
// GroupAPI an interface containing all methods associated with zendesk groups
3131
type GroupAPI interface {
3232
GetGroups(ctx context.Context, opts *GroupListOptions) ([]Group, Page, error)
33+
GetGroupsOBP(ctx context.Context, opts *OBPOptions) ([]Group, Page, error)
34+
GetGroupsCBP(ctx context.Context, opts *CBPOptions) ([]Group, CursorPaginationMeta, error)
35+
GetGroupsIterator(ctx context.Context, opts *PaginationOptions) *Iterator[Group]
3336
GetGroup(ctx context.Context, groupID int64) (Group, error)
3437
CreateGroup(ctx context.Context, group Group) (Group, error)
3538
UpdateGroup(ctx context.Context, groupID int64, group Group) (Group, error)
@@ -66,6 +69,72 @@ func (z *Client) GetGroups(ctx context.Context, opts *GroupListOptions) ([]Group
6669
return data.Groups, data.Page, nil
6770
}
6871

72+
// GetGroupsOBP fetches group list from OBP (Offset Based Pagination)
73+
// https://developer.zendesk.com/rest_api/docs/support/groups#list-groups
74+
func (z *Client) GetGroupsOBP(ctx context.Context, opts *OBPOptions) ([]Group, Page, error) {
75+
var data struct {
76+
Groups []Group `json:"groups"`
77+
Page
78+
}
79+
80+
tmp := opts
81+
if tmp == nil {
82+
tmp = &OBPOptions{}
83+
}
84+
85+
u, err := addOptions("/groups.json", tmp)
86+
if err != nil {
87+
return []Group{}, Page{}, err
88+
}
89+
90+
err = getData(z, ctx, u, &data)
91+
if err != nil {
92+
return []Group{}, Page{}, err
93+
}
94+
return data.Groups, data.Page, nil
95+
}
96+
97+
// GetGroupsIterator returns an Iterator to iterate over groups
98+
//
99+
// ref: https://developer.zendesk.com/rest_api/docs/support/groups#list-groups
100+
func (z *Client) GetGroupsIterator(ctx context.Context, opts *PaginationOptions) *Iterator[Group] {
101+
return &Iterator[Group]{
102+
pageSize: opts.PageSize,
103+
hasMore: true,
104+
isCBP: opts.IsCBP,
105+
pageAfter: "",
106+
pageIndex: 1,
107+
ctx: ctx,
108+
obpFunc: z.GetGroupsOBP,
109+
cbpFunc: z.GetGroupsCBP,
110+
}
111+
}
112+
113+
// GetGroupsCBP fetches group list from CBP (Cursor Based Pagination)
114+
// https://developer.zendesk.com/rest_api/docs/support/groups#list-groups
115+
func (z *Client) GetGroupsCBP(ctx context.Context, opts *CBPOptions) ([]Group, CursorPaginationMeta, error) {
116+
var data struct {
117+
Groups []Group `json:"groups"`
118+
Meta CursorPaginationMeta `json:"meta"`
119+
}
120+
121+
tmp := opts
122+
if tmp == nil {
123+
tmp = &CBPOptions{}
124+
}
125+
126+
u, err := addOptions("/groups.json", tmp)
127+
if err != nil {
128+
return []Group{}, data.Meta, err
129+
}
130+
131+
err = getData(z, ctx, u, &data)
132+
if err != nil {
133+
return []Group{}, data.Meta, err
134+
}
135+
return data.Groups, data.Meta, nil
136+
}
137+
69138
// CreateGroup creates new group
70139
// https://developer.zendesk.com/rest_api/docs/support/groups#create-group
71140
func (z *Client) CreateGroup(ctx context.Context, group Group) (Group, error) {

zendesk/group_test.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,33 @@ import (
66
"testing"
77
)
88

9+
func TestGetGroupsIterator(t *testing.T) {
10+
mockAPI := newMockAPI(http.MethodGet, "groups.json")
11+
client := newTestClient(mockAPI)
12+
defer mockAPI.Close()
13+
14+
ops := NewPaginationOptions()
15+
ops.PageSize = 10
16+
17+
it := client.GetGroupsIterator(ctx, ops)
18+
19+
expectedLength := 1
20+
groupsCount := 0
21+
for it.HasMore() {
22+
groups, err := it.GetNext()
23+
if len(groups) != expectedLength {
24+
t.Fatalf("expected length of groups is 1, but got %d", len(groups))
25+
}
26+
groupsCount += len(groups)
27+
if err != nil {
28+
t.Fatalf("Failed to get groups: %s", err)
29+
}
30+
}
31+
if groupsCount != 1 {
32+
t.Fatalf("expected length of groups is 1, but got %d", groupsCount)
33+
}
34+
}
35+
936
func TestGetGroups(t *testing.T) {
1037
mockAPI := newMockAPI(http.MethodGet, "groups.json")
1138
client := newTestClient(mockAPI)
@@ -21,6 +48,36 @@ func TestGetGroups(t *testing.T) {
2148
}
2249
}
2350

51+
func TestGetGroupsOBP(t *testing.T) {
52+
mockAPI := newMockAPI(http.MethodGet, "groups.json")
53+
client := newTestClient(mockAPI)
54+
defer mockAPI.Close()
55+
56+
groups, _, err := client.GetGroupsOBP(ctx, nil)
57+
if err != nil {
58+
t.Fatalf("Failed to get groups: %s", err)
59+
}
60+
61+
if len(groups) != 1 {
62+
t.Fatalf("expected length of groups is 1, but got %d", len(groups))
63+
}
64+
}
65+
66+
func TestGetGroupsCBP(t *testing.T) {
67+
mockAPI := newMockAPI(http.MethodGet, "groups.json")
68+
client := newTestClient(mockAPI)
69+
defer mockAPI.Close()
70+
71+
groups, _, err := client.GetGroupsCBP(ctx, nil)
72+
if err != nil {
73+
t.Fatalf("Failed to get groups: %s", err)
74+
}
75+
76+
if len(groups) != 1 {
77+
t.Fatalf("expected length of groups is 1, but got %d", len(groups))
78+
}
79+
}
80+
2481
func TestCreateGroup(t *testing.T) {
2582
mockAPI := newMockAPIWithStatus(http.MethodPost, "groups.json", http.StatusCreated)
2683
client := newTestClient(mockAPI)

zendesk/iterator.go

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
package zendesk
2+
3+
import (
4+
"context"
5+
)
6+
7+
// PaginationOptions struct represents general pagination options.
8+
// PageSize specifies the number of items per page, IsCBP indicates if it's cursor-based pagination,
9+
// SortBy and SortOrder describe how to sort the items in Offset Based Pagination, and Sort describes how to sort items in Cursor Based Pagination.
10+
type PaginationOptions struct {
11+
CommonOptions
12+
PageSize int //default is 100
13+
IsCBP bool //default is true
14+
}
15+
16+
// NewPaginationOptions() returns a pointer to a new PaginationOptions struct with default values (PageSize is 100, IsCBP is true).
17+
func NewPaginationOptions() *PaginationOptions {
18+
return &PaginationOptions{
19+
PageSize: 100,
20+
IsCBP: true,
21+
}
22+
}
23+
24+
type CommonOptions struct {
25+
Active bool `url:"active,omitempty"`
26+
Role string `url:"role,omitempty"`
27+
Roles []string `url:"role[],omitempty"`
28+
PermissionSet int64 `url:"permission_set,omitempty"`
29+
30+
// SortBy can take "assignee", "assignee.name", "created_at", "group", "id",
31+
// "locale", "requester", "requester.name", "status", "subject", "updated_at"
32+
SortBy string `url:"sort_by,omitempty"`
33+
34+
// SortOrder can take "asc" or "desc"
35+
SortOrder string `url:"sort_order,omitempty"`
36+
Sort string `url:"sort,omitempty"`
37+
Id int64
38+
}
39+
40+
// CBPOptions struct is used to specify options for listing objects in CBP (Cursor Based Pagination).
41+
// It embeds the CursorPagination struct for pagination and provides an option Sort for sorting the result.
42+
type CBPOptions struct {
43+
CursorPagination
44+
CommonOptions
45+
}
46+
47+
// OBPOptions struct is used to specify options for listing objects in OBP (Offset Based Pagination).
48+
// It embeds the PageOptions struct for pagination and provides options for sorting the result;
49+
// SortBy specifies the field to sort by, and SortOrder specifies the order (either 'asc' or 'desc').
50+
type OBPOptions struct {
51+
PageOptions
52+
CommonOptions
53+
}
54+
55+
// ObpFunc defines the signature of the function used to list objects in OBP.
56+
type ObpFunc[T any] func(ctx context.Context, opts *OBPOptions) ([]T, Page, error)
57+
58+
// CbpFunc defines the signature of the function used to list objects in CBP.
59+
type CbpFunc[T any] func(ctx context.Context, opts *CBPOptions) ([]T, CursorPaginationMeta, error)
60+
61+
// terator struct provides a convenient and genric way to iterate over pages of objects in either OBP or CBP.
62+
// It holds state for iteration, including the current page size, a flag indicating more pages, pagination type (OBP or CBP), and sorting options.
63+
type Iterator[T any] struct {
64+
CommonOptions
65+
// generic fields
66+
pageSize int
67+
hasMore bool
68+
isCBP bool
69+
70+
// OBP fields
71+
pageIndex int
72+
73+
// CBP fields
74+
pageAfter string
75+
76+
// common fields
77+
ctx context.Context
78+
obpFunc ObpFunc[T]
79+
cbpFunc CbpFunc[T]
80+
}
81+
82+
// HasMore() returns a boolean indicating whether more pages are available for iteration.
83+
func (i *Iterator[T]) HasMore() bool {
84+
return i.hasMore
85+
}
86+
87+
// GetNext() retrieves the next batch of objects according to the current pagination and sorting options.
88+
// It updates the state of the iterator for subsequent calls.
89+
// In case of an error, it sets hasMore to false and returns an error.
90+
func (i *Iterator[T]) GetNext() ([]T, error) {
91+
if !i.isCBP {
92+
obpOps := &OBPOptions{
93+
PageOptions: PageOptions{
94+
PerPage: i.pageSize,
95+
Page: i.pageIndex,
96+
},
97+
CommonOptions: i.CommonOptions,
98+
}
99+
results, page, err := i.obpFunc(i.ctx, obpOps)
100+
if err != nil {
101+
i.hasMore = false
102+
return nil, err
103+
}
104+
i.hasMore = page.HasNext()
105+
i.pageIndex++
106+
return results, nil
107+
}
108+
109+
cbpOps := &CBPOptions{
110+
CursorPagination: CursorPagination{
111+
PageSize: i.pageSize,
112+
PageAfter: i.pageAfter,
113+
},
114+
CommonOptions: i.CommonOptions,
115+
}
116+
results, meta, err := i.cbpFunc(i.ctx, cbpOps)
117+
if err != nil {
118+
i.hasMore = false
119+
return nil, err
120+
}
121+
i.hasMore = meta.HasMore
122+
i.pageAfter = meta.AfterCursor
123+
return results, nil
124+
}

0 commit comments

Comments
 (0)