Repository: nukosuke/go-zendesk
Pull request #285 (2023-09-20 05:53:10)

Added Cursor Based Pagination support for get tickets
1. Added Cursor Based Pagination support for get tickets method
2. Added Iterator for get tickets, by default, the iterator will use CBP to retrieve tickets.

Example to use the Iterator:


        ops := zendesk.NewPaginationOptions()
	it := client.GetTicketsEx(context.Background(), ops)

	for it.HasMore() {
		tickets, err := it.GetNext()
		if err == nil {
			for _, ticket := range tickets {
				println(ticket.Subject)
			}
		}
	}



Patch:
diff --git a/zendesk/mock/client.go b/zendesk/mock/client.go
index e8d8f54..30b2bbd 100644
--- a/zendesk/mock/client.go
+++ b/zendesk/mock/client.go
@@ -1179,6 +1179,35 @@ func (mr *ClientMockRecorder) GetTickets(arg0, arg1 interface{}) *gomock.Call {
 	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTickets", reflect.TypeOf((*Client)(nil).GetTickets), arg0, arg1)
 }
 
+// GetTicketsCBP mocks base method.
+func (m *Client) GetTicketsCBP(arg0 context.Context, arg1 *zendesk.TicketListCBPOptions) (*zendesk.TicketListCBPResult, error) {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetTicketsCBP", arg0, arg1)
+	ret0, _ := ret[0].(*zendesk.TicketListCBPResult)
+	ret1, _ := ret[1].(error)
+	return ret0, ret1
+}
+
+// GetTicketsCBP indicates an expected call of GetTicketsCBP.
+func (mr *ClientMockRecorder) GetTicketsCBP(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTicketsCBP", reflect.TypeOf((*Client)(nil).GetTicketsCBP), arg0, arg1)
+}
+
+// GetTicketsEx mocks base method.
+func (m *Client) GetTicketsEx(arg0 context.Context, arg1 *zendesk.PaginationOptions) *zendesk.TicketIterator {
+	m.ctrl.T.Helper()
+	ret := m.ctrl.Call(m, "GetTicketsEx", arg0, arg1)
+	ret0, _ := ret[0].(*zendesk.TicketIterator)
+	return ret0
+}
+
+// GetTicketsEx indicates an expected call of GetTicketsEx.
+func (mr *ClientMockRecorder) GetTicketsEx(arg0, arg1 interface{}) *gomock.Call {
+	mr.mock.ctrl.T.Helper()
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetTicketsEx", reflect.TypeOf((*Client)(nil).GetTicketsEx), arg0, arg1)
+}
+
 // GetTicketsFromView mocks base method.
 func (m *Client) GetTicketsFromView(arg0 context.Context, arg1 int64, arg2 *zendesk.TicketListOptions) ([]zendesk.Ticket, zendesk.Page, error) {
 	m.ctrl.T.Helper()
diff --git a/zendesk/ticket.go b/zendesk/ticket.go
index e0a7512..6aa6f9a 100644
--- a/zendesk/ticket.go
+++ b/zendesk/ticket.go
@@ -129,6 +129,9 @@ type Via struct {
 	} `json:"source"`
 }
 
+// TicketListOptions struct is used to specify options for listing tickets 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 TicketListOptions struct {
 	PageOptions
 
@@ -140,9 +143,119 @@ type TicketListOptions struct {
 	SortOrder string `url:"sort_order,omitempty"`
 }
 
+// TicketListCBPOptions struct is used to specify options for listing tickets in CBP (Cursor Based Pagination).
+// It embeds the CursorPagination struct for pagination and provides an option Sort for sorting the result.
+type TicketListCBPOptions struct {
+	CursorPagination
+	Sort string `url:"sort,omitempty"`
+}
+
+// TicketListCBPResult struct represents the result of a ticket list operation in CBP. It includes an array of Ticket objects, and Meta that holds pagination metadata.
+type TicketListCBPResult struct {
+	Tickets []Ticket             `json:"tickets"`
+	Meta    CursorPaginationMeta `json:"meta"`
+}
+
+// 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 {
+	PageSize int  //default is 100
+	IsCBP    bool //default is true
+
+	SortBy string
+	// SortOrder can take "asc" or "desc"
+	SortOrder string
+	Sort      string
+}
+
+// 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,
+	}
+}
+
+// TicketIterator struct provides a convenient way to iterate over pages of tickets 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 TicketIterator struct {
+	// generic fields
+	pageSize int
+	hasMore  bool
+	isCBP    bool
+
+	// OBP fields
+	sortBy string
+	// SortOrder can take "asc" or "desc"
+	sortOrder string
+	pageIndex int
+
+	// CBP fields
+	sort      string
+	pageAfter string
+
+	// common fields
+	client *Client
+	ctx    context.Context
+}
+
+// HasMore() returns a boolean indicating whether more pages are available for iteration.
+func (i *TicketIterator) HasMore() bool {
+	return i.hasMore
+}
+
+// GetNext() retrieves the next batch of tickets 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 *TicketIterator) GetNext() ([]Ticket, error) {
+	if i.isCBP {
+		cbpOps := &TicketListCBPOptions{
+			CursorPagination: CursorPagination{
+				PageSize:  i.pageSize,
+				PageAfter: i.pageAfter,
+			},
+		}
+		if i.sort != "" {
+			cbpOps.Sort = i.sort
+		}
+		ticketListCBPResult, err := i.client.GetTicketsCBP(i.ctx, cbpOps)
+		if err != nil {
+			i.hasMore = false
+			return nil, err
+		}
+		i.hasMore = ticketListCBPResult.Meta.HasMore
+		i.pageAfter = ticketListCBPResult.Meta.AfterCursor
+		return ticketListCBPResult.Tickets, nil
+	} else {
+		obpOps := &TicketListOptions{
+			PageOptions: PageOptions{
+				PerPage: i.pageSize,
+				Page:    i.pageIndex,
+			},
+		}
+		if i.sortBy != "" {
+			obpOps.SortBy = i.sortBy
+		}
+		if i.sortOrder != "" {
+			obpOps.SortOrder = i.sortOrder
+		}
+		tickets, page, err := i.client.GetTickets(i.ctx, obpOps)
+		if err != nil {
+			i.hasMore = false
+			return nil, err
+		}
+		i.hasMore = page.HasNext()
+		i.pageIndex++
+		return tickets, nil
+	}
+}
+
 // TicketAPI an interface containing all ticket related methods
 type TicketAPI interface {
+	GetTicketsEx(ctx context.Context, opts *PaginationOptions) *TicketIterator
 	GetTickets(ctx context.Context, opts *TicketListOptions) ([]Ticket, Page, error)
+	GetTicketsCBP(ctx context.Context, opts *TicketListCBPOptions) (*TicketListCBPResult, error)
 	GetOrganizationTickets(ctx context.Context, organizationID int64, ops *TicketListOptions) ([]Ticket, Page, error)
 	GetTicket(ctx context.Context, id int64) (Ticket, error)
 	GetMultipleTickets(ctx context.Context, ticketIDs []int64) ([]Ticket, error)
@@ -151,7 +264,25 @@ type TicketAPI interface {
 	DeleteTicket(ctx context.Context, ticketID int64) error
 }
 
-// GetTickets get ticket list
+// GetTicketsEx returns a TicketIterator to iterate over tickets
+//
+// ref: https://developer.zendesk.com/rest_api/docs/support/tickets#list-tickets
+func (z *Client) GetTicketsEx(ctx context.Context, opts *PaginationOptions) *TicketIterator {
+	return &TicketIterator{
+		pageSize:  opts.PageSize,
+		hasMore:   true,
+		isCBP:     opts.IsCBP,
+		sort:      opts.Sort,
+		pageAfter: "",
+		sortOrder: opts.SortOrder,
+		sortBy:    opts.SortBy,
+		pageIndex: 1,
+		client:    z,
+		ctx:       ctx,
+	}
+}
+
+// GetTickets get ticket list with offset based pagination
 //
 // ref: https://developer.zendesk.com/rest_api/docs/support/tickets#list-tickets
 func (z *Client) GetTickets(ctx context.Context, opts *TicketListOptions) ([]Ticket, Page, error) {
@@ -182,6 +313,34 @@ func (z *Client) GetTickets(ctx context.Context, opts *TicketListOptions) ([]Tic
 	return data.Tickets, data.Page, nil
 }
 
+// GetTicketsCBP get ticket list with cursor based pagination
+//
+// ref: https://developer.zendesk.com/rest_api/docs/support/tickets#list-tickets
+func (z *Client) GetTicketsCBP(ctx context.Context, opts *TicketListCBPOptions) (*TicketListCBPResult, error) {
+	var data TicketListCBPResult
+
+	tmp := opts
+	if tmp == nil {
+		tmp = &TicketListCBPOptions{}
+	}
+
+	u, err := addOptions("/tickets.json", tmp)
+	if err != nil {
+		return nil, err
+	}
+
+	body, err := z.get(ctx, u)
+	if err != nil {
+		return nil, err
+	}
+
+	err = json.Unmarshal(body, &data)
+	if err != nil {
+		return nil, err
+	}
+	return &data, nil
+}
+
 // GetOrganizationTickets get organization ticket list
 //
 // ref: https://developer.zendesk.com/rest_api/docs/support/tickets#list-tickets


Test patch:
diff --git a/zendesk/ticket_test.go b/zendesk/ticket_test.go
index 31d1e88..222f326 100644
--- a/zendesk/ticket_test.go
+++ b/zendesk/ticket_test.go
@@ -34,6 +34,76 @@ func TestGetTickets(t *testing.T) {
 	}
 }
 
+func TestGetTicketsCBP(t *testing.T) {
+	mockAPI := newMockAPI(http.MethodGet, "tickets.json")
+	client := newTestClient(mockAPI)
+	defer mockAPI.Close()
+
+	tickets, err := client.GetTicketsCBP(ctx, &TicketListCBPOptions{
+		CursorPagination: CursorPagination{
+			PageSize:  10,
+			PageAfter: "",
+		},
+	})
+	if err != nil {
+		t.Fatalf("Failed to get tickets: %s", err)
+	}
+
+	expectedLength := 2
+	if len(tickets.Tickets) != expectedLength {
+		t.Fatalf("Returned tickets does not have the expected length %d. Tickets length is %d", expectedLength, len(tickets.Tickets))
+	}
+}
+
+func TestGetTicketsExCBPDefault(t *testing.T) {
+	mockAPI := newMockAPI(http.MethodGet, "tickets.json")
+	client := newTestClient(mockAPI)
+	defer mockAPI.Close()
+
+	ops := NewPaginationOptions()
+	it := client.GetTicketsEx(ctx, ops)
+
+	expectedLength := 2
+	ticketCount := 0
+	for it.HasMore() {
+		tickets, err := it.GetNext()
+		if err == nil {
+			for _, ticket := range tickets {
+				println(ticket.Subject)
+				ticketCount++
+			}
+		}
+	}
+	if ticketCount != expectedLength {
+		t.Fatalf("Returned tickets does not have the expected length %d. Tickets length is %d", expectedLength, ticketCount)
+	}
+}
+
+func TestGetTicketsExOBPOptional(t *testing.T) {
+	mockAPI := newMockAPI(http.MethodGet, "tickets.json")
+	client := newTestClient(mockAPI)
+	defer mockAPI.Close()
+
+	ops := NewPaginationOptions()
+	ops.IsCBP = false
+	it := client.GetTicketsEx(ctx, ops)
+
+	expectedLength := 2
+	ticketCount := 0
+	for it.HasMore() {
+		tickets, err := it.GetNext()
+		if err == nil {
+			for _, ticket := range tickets {
+				println(ticket.Subject)
+				ticketCount++
+			}
+		}
+	}
+	if ticketCount != expectedLength {
+		t.Fatalf("Returned tickets does not have the expected length %d. Tickets length is %d", expectedLength, ticketCount)
+	}
+}
+
 func TestGetOrganizationTickets(t *testing.T) {
 	mockAPI := newMockAPI(http.MethodGet, "tickets.json")
 	client := newTestClient(mockAPI)