Repository: nukosuke/go-zendesk
Pull request #262 (2023-02-15 16:52:03)

Add cursor pagination for list ticket comments.
Addresses #261 


Patch:
diff --git a/fixture/GET/ticket_comments.json b/fixture/GET/ticket_comments.json
index f3edd4a..3faa564 100644
--- a/fixture/GET/ticket_comments.json
+++ b/fixture/GET/ticket_comments.json
@@ -22,5 +22,10 @@
 		  "attachments": [],
 		  "created_at": "2019-06-03T02:23:47Z"
 	  }
-  ]
+  ],
+  "meta": {
+    "has_more": true,
+    "after_cursor": "xxx",
+    "before_cursor": "yyy"
+  }
 }
diff --git a/zendesk/cursor.go b/zendesk/cursor.go
index f1d40de..6999474 100644
--- a/zendesk/cursor.go
+++ b/zendesk/cursor.go
@@ -1,5 +1,8 @@
 package zendesk
 
+// DEPRECATED -- This file describes Zendesk API's "legacy" cursor pagination.
+// PLEASE USE CursorPagination IN zendesk.go INSTEAD!
+
 // Cursor is struct for cursor-based pagination
 type Cursor struct {
 	AfterURL     string `json:"after_url"`
diff --git a/zendesk/error.go b/zendesk/error.go
index 7aef7ed..f0024ca 100644
--- a/zendesk/error.go
+++ b/zendesk/error.go
@@ -17,8 +17,8 @@ type Error struct {
 // NewError is a function to initialize the Error type. This function will be useful
 // for unit testing and mocking purposes in the client side
 // to test their behavior by the API response.
-func NewError(body []byte, resp *http.Response) Error {
-	return Error{
+func NewError(body []byte, resp *http.Response) *Error {
+	return &Error{
 		body: body,
 		resp: resp,
 	}
diff --git a/zendesk/mock/client.go b/zendesk/mock/client.go
index 19f76d3..05723ef 100644
--- a/zendesk/mock/client.go
+++ b/zendesk/mock/client.go
@@ -1286,18 +1286,18 @@ func (mr *ClientMockRecorder) ListInstallations(arg0 interface{}) *gomock.Call {
 }
 
 // ListTicketComments mocks base method.
-func (m *Client) ListTicketComments(arg0 context.Context, arg1 int64) ([]zendesk.TicketComment, error) {
+func (m *Client) ListTicketComments(arg0 context.Context, arg1 int64, arg2 *zendesk.ListTicketCommentsOptions) (*zendesk.ListTicketCommentsResult, error) {
 	m.ctrl.T.Helper()
-	ret := m.ctrl.Call(m, "ListTicketComments", arg0, arg1)
-	ret0, _ := ret[0].([]zendesk.TicketComment)
+	ret := m.ctrl.Call(m, "ListTicketComments", arg0, arg1, arg2)
+	ret0, _ := ret[0].(*zendesk.ListTicketCommentsResult)
 	ret1, _ := ret[1].(error)
 	return ret0, ret1
 }
 
 // ListTicketComments indicates an expected call of ListTicketComments.
-func (mr *ClientMockRecorder) ListTicketComments(arg0, arg1 interface{}) *gomock.Call {
+func (mr *ClientMockRecorder) ListTicketComments(arg0, arg1, arg2 interface{}) *gomock.Call {
 	mr.mock.ctrl.T.Helper()
-	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTicketComments", reflect.TypeOf((*Client)(nil).ListTicketComments), arg0, arg1)
+	return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ListTicketComments", reflect.TypeOf((*Client)(nil).ListTicketComments), arg0, arg1, arg2)
 }
 
 // MakeCommentPrivate mocks base method.
diff --git a/zendesk/ticket_comment.go b/zendesk/ticket_comment.go
index e1c14b2..1bca1d3 100644
--- a/zendesk/ticket_comment.go
+++ b/zendesk/ticket_comment.go
@@ -10,7 +10,7 @@ import (
 // TicketCommentAPI is an interface containing all ticket comment related API methods
 type TicketCommentAPI interface {
 	CreateTicketComment(ctx context.Context, ticketID int64, ticketComment TicketComment) (TicketComment, error)
-	ListTicketComments(ctx context.Context, ticketID int64) ([]TicketComment, error)
+	ListTicketComments(ctx context.Context, ticketID int64, opts *ListTicketCommentsOptions) (*ListTicketCommentsResult, error)
 	MakeCommentPrivate(ctx context.Context, ticketID int64, ticketCommentID int64) error
 }
 
@@ -82,25 +82,65 @@ func (z *Client) CreateTicketComment(ctx context.Context, ticketID int64, ticket
 	return result, err
 }
 
+type listTicketCommentsSort string
+
+const (
+	// TicketCommentCreatedAtAsc defines ASC sort val.
+	TicketCommentCreatedAtAsc listTicketCommentsSort = "created_at"
+
+	// TicketCommentCreatedAtDesc defines DESC sort val.
+	TicketCommentCreatedAtDesc listTicketCommentsSort = "-created_at"
+
+	// ListTicketCommentsMaxPageSize contains the max page size.
+	ListTicketCommentsMaxPageSize int = 100
+)
+
+// ListTicketCommentOptions contains all the options supported by ListTicketComments endpoint.
+type ListTicketCommentsOptions struct {
+	CursorPagination
+
+	Include             string                 `url:"include,omitempty"`
+	IncludeInlineImages string                 `url:"include_inline_images,omitempty"`
+	Sort                listTicketCommentsSort `url:"sort,omitempty"`
+}
+
+// ListTicketCommentsResult contains the resulting ticket comments
+// and cursor pagination metadata.
+type ListTicketCommentsResult struct {
+	TicketComments []TicketComment      `json:"comments"`
+	Meta           CursorPaginationMeta `json:"meta"`
+}
+
 // ListTicketComments gets a list of comment for a specified ticket
 //
 // ref: https://developer.zendesk.com/rest_api/docs/support/ticket_comments#list-comments
-func (z *Client) ListTicketComments(ctx context.Context, ticketID int64) ([]TicketComment, error) {
-	var result struct {
-		TicketComments []TicketComment `json:"comments"`
+func (z *Client) ListTicketComments(
+	ctx context.Context,
+	ticketID int64,
+	opts *ListTicketCommentsOptions,
+) (*ListTicketCommentsResult, error) {
+	url := fmt.Sprintf("/tickets/%d/comments.json", ticketID)
+
+	var err error
+	if opts != nil {
+		url, err = addOptions(url, opts)
+		if err != nil {
+			return nil, err
+		}
 	}
 
-	body, err := z.get(ctx, fmt.Sprintf("/tickets/%d/comments.json", ticketID))
+	body, err := z.get(ctx, url)
 	if err != nil {
-		return []TicketComment{}, err
+		return nil, err
 	}
 
+	var result ListTicketCommentsResult
 	err = json.Unmarshal(body, &result)
 	if err != nil {
-		return []TicketComment{}, err
+		return nil, err
 	}
 
-	return result.TicketComments, err
+	return &result, err
 }
 
 // MakeCommentPrivate converts an existing ticket comment to an internal note that is not publicly viewable.
diff --git a/zendesk/zendesk.go b/zendesk/zendesk.go
index 615526e..187d6fc 100644
--- a/zendesk/zendesk.go
+++ b/zendesk/zendesk.go
@@ -40,6 +40,33 @@ type (
 		Put(ctx context.Context, path string, data interface{}) ([]byte, error)
 		Delete(ctx context.Context, path string) error
 	}
+
+	// CursorPagination contains options for using cursor pagination.
+	// Cursor pagination is preferred where possible.
+	CursorPagination struct {
+		// PageSize sets the number of results per page.
+		// Most endpoints support up to 100 records per page.
+		PageSize int `url:"page[size],omitempty"`
+
+		// PageAfter provides the "next" cursor.
+		PageAfter string `url:"page[after],omitempty"`
+
+		// PageBefore provides the "previous" cursor.
+		PageBefore string `url:"page[before],omitempty"`
+	}
+
+	// CursorPaginationMeta contains information concerning how to fetch
+	// next and previous results, and if next results exist.
+	CursorPaginationMeta struct {
+		// HasMore is true if more results exist in the endpoint.
+		HasMore bool `json:"has_more,omitempty"`
+
+		// AfterCursor contains the cursor of the next result set.
+		AfterCursor string `json:"after_cursor,omitempty"`
+
+		// BeforeCursor contains the cursor of the previous result set.
+		BeforeCursor string `json:"before_cursor,omitempty"`
+	}
 )
 
 // NewClient creates new Zendesk API client


Test patch:
diff --git a/zendesk/ticket_comment_test.go b/zendesk/ticket_comment_test.go
index 731cef0..e0cc4b8 100644
--- a/zendesk/ticket_comment_test.go
+++ b/zendesk/ticket_comment_test.go
@@ -43,14 +43,26 @@ func TestListTicketComments(t *testing.T) {
 	client := newTestClient(mockAPI)
 	defer mockAPI.Close()
 
-	ticketComments, err := client.ListTicketComments(ctx, 2)
+	result, err := client.ListTicketComments(ctx, 2, nil)
 	if err != nil {
 		t.Fatalf("Failed to list ticket comments: %s", err)
 	}
 
 	expectedLength := 2
-	if len(ticketComments) != expectedLength {
-		t.Fatalf("Returned ticket comments does not have the expected length %d. Ticket comments length is %d", expectedLength, len(ticketComments))
+	if len(result.TicketComments) != expectedLength {
+		t.Fatalf("Returned ticket comments does not have the expected length %d. Ticket comments length is %d", expectedLength, len(result.TicketComments))
+	}
+
+	expectedPaginationMeta := CursorPaginationMeta{
+		HasMore:      true,
+		AfterCursor:  "xxx",
+		BeforeCursor: "yyy",
+	}
+
+	if result.Meta != expectedPaginationMeta {
+		t.Fatalf(`Failed to return correct cursor options.
+Expected: %+v
+Received: %+v`, expectedPaginationMeta, result.Meta)
 	}
 }