Skip to content

Commit 813ba06

Browse files
committed
Created dedicated page response objects for paginated queries
1 parent 9984056 commit 813ba06

File tree

5 files changed

+100
-44
lines changed

5 files changed

+100
-44
lines changed

kotlin-jira-client/kotlin-jira-client-api/src/main/kotlin/com/linkedplanet/kotlinjiraclient/api/interfaces/JiraIssueOperator.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import arrow.core.Either
2323
import com.google.gson.JsonObject
2424
import com.linkedplanet.kotlinjiraclient.api.error.JiraClientError
2525
import com.linkedplanet.kotlinjiraclient.api.model.JiraIssue
26+
import com.linkedplanet.kotlinjiraclient.api.model.Page
2627

2728
/**
2829
* Provides methods for working with Jira issues, including retrieving issues by JQL query, issue type, or key; creating and updating issues; and deleting issues.
@@ -55,7 +56,7 @@ interface JiraIssueOperator<JiraFieldType> {
5556
pageIndex: Int = 0,
5657
pageSize: Int = RESULTS_PER_PAGE,
5758
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
58-
): Either<JiraClientError, List<T>>
59+
): Either<JiraClientError, Page<T>>
5960

6061
/**
6162
* Returns an issue based on a JQL query.
@@ -96,7 +97,7 @@ interface JiraIssueOperator<JiraFieldType> {
9697
pageIndex: Int = 0,
9798
pageSize: Int = RESULTS_PER_PAGE,
9899
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
99-
): Either<JiraClientError, List<T>>
100+
): Either<JiraClientError, Page<T>>
100101

101102
/**
102103
* Returns an issue by key.

kotlin-jira-client/kotlin-jira-client-api/src/main/kotlin/com/linkedplanet/kotlinjiraclient/api/model/Model.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,14 @@
1919
*/
2020
package com.linkedplanet.kotlinjiraclient.api.model
2121

22+
data class Page<T> (
23+
val items: List<T>,
24+
val totalItems: Int,
25+
val totalPages: Int,
26+
val currentPageIndex: Int,
27+
val pageSize: Int
28+
)
29+
2230
data class JiraUser(
2331
val key: String,
2432
val name: String,

kotlin-jira-client/kotlin-jira-client-http/src/main/kotlin/com/linkedplanet/kotlinjiraclient/http/HttpJiraIssueOperator.kt

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,13 @@ import com.linkedplanet.kotlinhttpclient.api.http.*
2727
import com.linkedplanet.kotlinjiraclient.api.error.JiraClientError
2828
import com.linkedplanet.kotlinjiraclient.api.interfaces.JiraIssueOperator
2929
import com.linkedplanet.kotlinjiraclient.api.model.JiraIssue
30+
import com.linkedplanet.kotlinjiraclient.api.model.Page
3031
import com.linkedplanet.kotlinjiraclient.http.field.HttpJiraField
3132
import com.linkedplanet.kotlinjiraclient.http.model.HttpMappingField
3233
import com.linkedplanet.kotlinjiraclient.http.util.fromHttpDomainError
3334
import kotlinx.coroutines.runBlocking
35+
import kotlin.math.ceil
36+
import kotlin.math.floor
3437

3538
class HttpJiraIssueOperator(private val context: HttpJiraClientContext) : JiraIssueOperator<HttpJiraField> {
3639

@@ -73,7 +76,7 @@ class HttpJiraIssueOperator(private val context: HttpJiraClientContext) : JiraIs
7376
pageIndex: Int,
7477
pageSize: Int,
7578
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
76-
): Either<JiraClientError, List<T>> = either {
79+
): Either<JiraClientError, Page<T>> = either {
7780
val page = context.httpClient.executeGet<HttpJiraIssuePage>(
7881
"/rest/api/2/search",
7982
mapOf(
@@ -88,18 +91,29 @@ class HttpJiraIssueOperator(private val context: HttpJiraClientContext) : JiraIs
8891
.mapLeft { JiraClientError.fromHttpDomainError(it) }
8992
.bind()
9093

91-
if (page.getTotal().toInt() < 1) {
94+
val issues = if (page.getTotal().toInt() < 1) {
9295
emptyList()
9396
} else {
9497
parseIssues(page, parser).bind()
9598
}
99+
100+
val total = page.getTotal()
101+
val startAt = page.getStartAt()
102+
val maxResults = page.getMaxResults()
103+
Page(
104+
issues,
105+
total.toInt(),
106+
ceil(total.toDouble() / maxResults.toDouble()).toInt(),
107+
floor(startAt.toDouble() / maxResults.toDouble()).toInt(),
108+
maxResults.toInt()
109+
)
96110
}
97111

98112
override suspend fun <T> getIssueByJQL(
99113
jql: String,
100114
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
101115
): Either<JiraClientError, T?> = either {
102-
getIssuesByJQLPaginated(jql, 0, 1, parser).bind().firstOrNull()
116+
getIssuesByJQLPaginated(jql, 0, 1, parser).bind().items.firstOrNull()
103117
}
104118

105119
override suspend fun <T> getIssuesByIssueType(
@@ -116,7 +130,7 @@ class HttpJiraIssueOperator(private val context: HttpJiraClientContext) : JiraIs
116130
pageIndex: Int,
117131
pageSize: Int,
118132
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
119-
): Either<JiraClientError, List<T>> = either {
133+
): Either<JiraClientError, Page<T>> = either {
120134
getIssuesByJQLPaginated("project=$projectId AND issueType=$issueTypeId", pageIndex, pageSize, parser).bind()
121135
}
122136

kotlin-jira-client/kotlin-jira-client-sdk/src/main/kotlin/com/linkedplanet/kotlinjiraclient/sdk/SdkJiraIssueOperator.kt

Lines changed: 22 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package com.linkedplanet.kotlinjiraclient.sdk
2121

2222
import arrow.core.*
23+
import arrow.core.computations.either
2324
import com.atlassian.jira.bc.issue.search.SearchService
2425
import com.atlassian.jira.component.ComponentAccessor
2526
import com.atlassian.jira.event.type.EventDispatchOption
@@ -31,10 +32,12 @@ import com.google.gson.JsonObject
3132
import com.linkedplanet.kotlinjiraclient.api.error.JiraClientError
3233
import com.linkedplanet.kotlinjiraclient.api.interfaces.JiraIssueOperator
3334
import com.linkedplanet.kotlinjiraclient.api.model.JiraIssue
35+
import com.linkedplanet.kotlinjiraclient.api.model.Page
3436
import com.linkedplanet.kotlinjiraclient.sdk.field.SdkJiraField
3537
import com.linkedplanet.kotlinjiraclient.sdk.util.IssueJsonConverter
3638
import com.linkedplanet.kotlinjiraclient.sdk.util.catchJiraClientError
3739
import javax.inject.Named
40+
import kotlin.math.ceil
3841

3942
@Named
4043
object SdkJiraIssueOperator : JiraIssueOperator<SdkJiraField> {
@@ -102,12 +105,12 @@ object SdkJiraIssueOperator : JiraIssueOperator<SdkJiraField> {
102105
override suspend fun <T> getIssueByJQL(
103106
jql: String,
104107
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
105-
): Either<JiraClientError, T?> {
106-
val potentiallyMultipleIssues = getIssuesByJQLPaginated(jql, 0, 1, parser)
107-
if (potentiallyMultipleIssues.isRight() && potentiallyMultipleIssues.orNull()!!.isEmpty()) {
108-
return JiraClientError("Issue not found", "No issue was found.").left()
108+
): Either<JiraClientError, T?> = either {
109+
val potentiallyMultipleIssues = getIssuesByJQLPaginated(jql, 0, 1, parser).bind()
110+
if (potentiallyMultipleIssues.totalItems < 1) {
111+
JiraClientError("Issue not found", "No issue was found.").left().bind()
109112
}
110-
return potentiallyMultipleIssues.map { it.firstOrNull() }
113+
potentiallyMultipleIssues.items.first()
111114
}
112115

113116
override suspend fun <T> getIssuesByIssueType(
@@ -123,7 +126,7 @@ object SdkJiraIssueOperator : JiraIssueOperator<SdkJiraField> {
123126
pageIndex: Int,
124127
pageSize: Int,
125128
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
126-
): Either<JiraClientError, List<T>> =
129+
): Either<JiraClientError, Page<T>> =
127130
getIssuesByJQLPaginated("project=$projectId AND issueType=$issueTypeId", pageIndex, pageSize, parser)
128131

129132
override suspend fun <T> getIssueByKey(
@@ -139,15 +142,17 @@ object SdkJiraIssueOperator : JiraIssueOperator<SdkJiraField> {
139142
override suspend fun <T> getIssuesByJQL(
140143
jql: String,
141144
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
142-
): Either<JiraClientError, List<T>> =
143-
getIssuesByJqlWithPagerFilter(jql, PagerFilter.getUnlimitedFilter(), parser)
145+
): Either<JiraClientError, List<T>> = either {
146+
val issuePage = getIssuesByJqlWithPagerFilter(jql, PagerFilter.getUnlimitedFilter(), parser).bind()
147+
issuePage.items
148+
}
144149

145150
override suspend fun <T> getIssuesByJQLPaginated(
146151
jql: String,
147152
pageIndex: Int,
148153
pageSize: Int,
149154
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
150-
): Either<JiraClientError, List<T>> =
155+
): Either<JiraClientError, Page<T>> =
151156
getIssuesByJqlWithPagerFilter(jql, PagerFilter.newPageAlignedFilter(pageIndex * pageSize, pageSize), parser)
152157

153158
private suspend fun <T> issueToConcreteType(
@@ -163,11 +168,17 @@ object SdkJiraIssueOperator : JiraIssueOperator<SdkJiraField> {
163168
jql: String,
164169
pagerFilter: PagerFilter<*>?,
165170
parser: suspend (JsonObject, Map<String, String>) -> Either<JiraClientError, T>
166-
): Either<JiraClientError, List<T>> {
171+
): Either<JiraClientError, Page<T>> = either {
167172
val query = jqlParser.parseQuery(jql)
168173
val search = searchService.search(user(), query, pagerFilter)
169-
return search.results
174+
val issues = search.results
170175
.map { issue -> issueToConcreteType(issue, parser) }
171176
.sequenceEither()
177+
.bind()
178+
val totalItems = search.total
179+
val pageSize = pagerFilter?.pageSize ?: 0
180+
val totalPages = ceil(totalItems.toDouble() / pageSize.toDouble()).toInt()
181+
val currentPageIndex = pagerFilter?.start?.let { start -> start / pageSize } ?: 0
182+
Page(issues, totalItems, totalPages, currentPageIndex, pageSize)
172183
}
173184
}

kotlin-jira-client/kotlin-jira-client-test-base/src/main/kotlin/com/linkedplanet/kotlinjiraclient/JiraIssueOperatorTest.kt

Lines changed: 49 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
package com.linkedplanet.kotlinjiraclient
2121

2222
import arrow.core.*
23+
import com.linkedplanet.kotlinjiraclient.api.model.Page
2324
import com.linkedplanet.kotlinjiraclient.util.*
2425
import java.time.ZoneOffset
2526
import java.time.ZonedDateTime
@@ -100,23 +101,28 @@ interface JiraIssueOperatorTest<JiraFieldType> : BaseTestConfigProvider<JiraFiel
100101
@Test
101102
fun issues_04GetIssuesByJQLPaginated() {
102103
println("### START issues_04GetIssuesByJQLPaginated")
103-
val pages = 1..10
104-
val issues = pages.flatMap { pageNumber ->
105-
val page: List<Story> = runBlocking {
104+
// 10 items with page size 1 -> 10 pages
105+
val pageNumbers = 1..10
106+
val pages = pageNumbers.map { pageNumber ->
107+
val page: Page<Story>? = runBlocking {
106108
issueOperator.getIssuesByJQLPaginated(
107109
"summary ~ \"Test-*\"",
108110
pageNumber - 1,
109111
1,
110112
parser = ::issueParser
111-
).orNull() ?: emptyList()
113+
).orNull()
112114
}
113-
assertEquals(1, page.size)
115+
assertNotNull(page)
116+
assertEquals(10, page!!.totalItems)
117+
assertEquals(10, page.totalPages)
118+
assertEquals(pageNumber - 1, page.currentPageIndex)
119+
assertEquals(1, page.pageSize)
114120
page
115121
}
116-
assertEquals(10, issues.size)
117-
val issueKeys = (1..10)
118-
issueKeys.forEach { issueKey ->
119-
val issue = issues.singleOrNull { it.summary == "Test-$issueKey" }
122+
assertEquals(10, pages.size)
123+
124+
pageNumbers.forEach { issueKey ->
125+
val issue = pages.flatMap { it.items }.singleOrNull { it.summary == "Test-$issueKey" }
120126
assertNotNull(issue)
121127
assertEquals("IT-1", issue!!.insightObjectKey)
122128
assertEquals("To Do", issue.status.name)
@@ -127,23 +133,29 @@ interface JiraIssueOperatorTest<JiraFieldType> : BaseTestConfigProvider<JiraFiel
127133
@Test
128134
fun issues_05GetIssuesByJQLPaginated() {
129135
println("### START issues_05GetIssuesByJQLPaginated")
130-
val pages = 1..5
131-
val issues = pages.flatMap { pageNumber ->
136+
// 10 items with page size 2 -> 5 pages
137+
val pageNumbers = 1..5
138+
val pages = pageNumbers.map { pageNumber ->
132139
val page = runBlocking {
133140
issueOperator.getIssuesByJQLPaginated(
134141
"summary ~ \"Test-*\"",
135142
pageNumber - 1,
136143
2,
137144
parser = ::issueParser
138-
).orNull() ?: emptyList()
145+
).orNull()
139146
}
140-
assertEquals(2, page.size)
147+
assertNotNull(page)
148+
assertEquals(10, page!!.totalItems)
149+
assertEquals(5, page.totalPages)
150+
assertEquals(pageNumber - 1, page.currentPageIndex)
151+
assertEquals(2, page.pageSize)
141152
page
142153
}
143-
assertEquals(10, issues.size)
154+
assertEquals(5, pages.size)
155+
144156
val issueKeys = (1..10)
145157
issueKeys.forEach { issueKey ->
146-
val issue = issues.singleOrNull { it.summary == "Test-$issueKey" }
158+
val issue = pages.flatMap { it.items }.singleOrNull { it.summary == "Test-$issueKey" }
147159
assertNotNull(issue)
148160
assertEquals("IT-1", issue!!.insightObjectKey)
149161
assertEquals("To Do", issue.status.name)
@@ -154,23 +166,30 @@ interface JiraIssueOperatorTest<JiraFieldType> : BaseTestConfigProvider<JiraFiel
154166
@Test
155167
fun issues_06GetIssuesByIssueTypePaginated() {
156168
println("### START issues_06GetIssuesByIssueTypePaginated")
157-
val pages = 1..10
158-
val issues = pages.flatMap { page ->
169+
// 10 items with page size 3 -> 4 pages
170+
val pageNumbers = 1..4
171+
val pages = pageNumbers.map { pageNumber ->
159172
runBlocking {
160-
issueOperator.getIssuesByTypePaginated(
173+
val page = issueOperator.getIssuesByTypePaginated(
161174
projectId,
162175
issueTypeId,
163-
page - 1,
164-
1,
176+
pageNumber - 1,
177+
3,
165178
parser = ::issueParser
166-
)
167-
.orNull() ?: emptyList()
179+
).orNull()
180+
assertNotNull(page)
181+
assertEquals(10, page!!.totalItems)
182+
assertEquals(4, page.totalPages)
183+
assertEquals(pageNumber - 1, page.currentPageIndex)
184+
assertEquals(3, page.pageSize)
185+
page
168186
}
169187
}
170-
assertEquals(10, issues.size)
188+
assertEquals(4, pages.size)
189+
171190
val issueKeys = (1..10)
172191
issueKeys.forEach { issueKey ->
173-
val issue = issues.singleOrNull { it.summary == "Test-$issueKey" }
192+
val issue = pages.flatMap { it.items }.singleOrNull { it.summary == "Test-$issueKey" }
174193
assertNotNull(issue)
175194
assertEquals("IT-1", issue!!.insightObjectKey)
176195
assertEquals("To Do", issue.status.name)
@@ -384,12 +403,15 @@ interface JiraIssueOperatorTest<JiraFieldType> : BaseTestConfigProvider<JiraFiel
384403
@Test
385404
fun issues_13GetIssuesByJQLPaginatedEmpty() {
386405
println("### START issues_13GetIssuesByJQLPaginatedEmpty")
387-
val issues: List<Story>? = runBlocking {
406+
val page = runBlocking {
388407
issueOperator.getIssuesByJQLPaginated("summary ~ \"Emptyyyyy-*\"", parser = ::issueParser).orNull()
389408
}
390409

391-
assertNotNull(issues)
392-
assertTrue(issues!!.isEmpty())
410+
assertNotNull(page)
411+
assertEquals(0, page!!.totalItems)
412+
assertEquals(0, page.totalPages)
413+
assertEquals(0, page.currentPageIndex)
414+
assertTrue(page.items.isEmpty())
393415

394416
println("### END issues_13GetIssuesByJQLPaginatedEmpty")
395417
}

0 commit comments

Comments
 (0)