Skip to content

Commit a73a6fe

Browse files
committed
add getWithNext, getWithNext to SpotifyRestAction, and associated tests
1 parent 673c40a commit a73a6fe

File tree

9 files changed

+184
-51
lines changed

9 files changed

+184
-51
lines changed

build.gradle.kts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ kotlin {
102102
}
103103
}
104104

105+
all {
106+
languageSettings.useExperimentalAnnotation("kotlin.Experimental")
107+
}
105108
}
106109
}
107110
}

src/commonMain/kotlin/com.adamratzman.spotify/SpotifyApi.kt

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,10 @@ import com.adamratzman.spotify.models.TokenValidityResponse
2828
import com.adamratzman.spotify.models.serialization.toObject
2929
import com.adamratzman.spotify.utils.asList
3030
import com.adamratzman.spotify.utils.runBlocking
31-
import kotlin.coroutines.CoroutineContext
32-
import kotlin.jvm.JvmOverloads
3331
import kotlinx.coroutines.Dispatchers
3432
import kotlinx.serialization.json.Json
33+
import kotlin.coroutines.CoroutineContext
34+
import kotlin.jvm.JvmOverloads
3535

3636
/**
3737
* Base url for Spotify web api calls
@@ -695,4 +695,4 @@ internal suspend fun executeTokenRequest(
695695
)
696696
)
697697
)
698-
}
698+
}

src/commonMain/kotlin/com.adamratzman.spotify/SpotifyRestAction.kt

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,12 @@
11
/* Spotify Web API, Kotlin Wrapper; MIT License, 2017-2020; Original author: Adam Ratzman */
22
package com.adamratzman.spotify
33

4+
import com.adamratzman.spotify.annotations.SpotifyExperimentalHttpApi
45
import com.adamratzman.spotify.models.AbstractPagingObject
56
import com.adamratzman.spotify.utils.TimeUnit
67
import com.adamratzman.spotify.utils.getCurrentTimeMs
78
import com.adamratzman.spotify.utils.runBlocking
89
import com.adamratzman.spotify.utils.schedule
9-
import kotlin.coroutines.CoroutineContext
10-
import kotlin.coroutines.resume
11-
import kotlin.coroutines.resumeWithException
12-
import kotlin.coroutines.suspendCoroutine
13-
import kotlin.jvm.JvmOverloads
1410
import kotlinx.coroutines.CancellationException
1511
import kotlinx.coroutines.CoroutineScope
1612
import kotlinx.coroutines.Dispatchers
@@ -25,6 +21,11 @@ import kotlinx.coroutines.flow.flow
2521
import kotlinx.coroutines.flow.flowOn
2622
import kotlinx.coroutines.launch
2723
import kotlinx.coroutines.withContext
24+
import kotlin.coroutines.CoroutineContext
25+
import kotlin.coroutines.resume
26+
import kotlin.coroutines.resumeWithException
27+
import kotlin.coroutines.suspendCoroutine
28+
import kotlin.jvm.JvmOverloads
2829

2930
/**
3031
* Provides a uniform interface to retrieve, whether synchronously or asynchronously, [T] from Spotify
@@ -139,6 +140,24 @@ class SpotifyRestActionPaging<Z : Any, T : AbstractPagingObject<Z>>(api: Spotify
139140
*/
140141
fun getAll(context: CoroutineContext = Dispatchers.Default) = api.tracks.toAction { suspendComplete(context).getAllImpl() }
141142

143+
/**
144+
* Synchronously retrieve the next [total] paging objects associated with this rest action, including the current one.
145+
*
146+
* @param total The total amount of [AbstractPagingObject] to request, including the [AbstractPagingObject] associated with the current request.
147+
* @since 3.0.0
148+
*/
149+
@SpotifyExperimentalHttpApi
150+
fun getWithNext(total: Int, context: CoroutineContext = Dispatchers.Default) = api.tracks.toAction { suspendComplete(context).getWithNextImpl(total) }
151+
152+
/**
153+
* Synchronously retrieve the items associated with the next [total] paging objects associated with this rest action, including the current one.
154+
*
155+
* @param total The total amount of [AbstractPagingObject] to request, including the [AbstractPagingObject] associated with the current request.
156+
* @since 3.0.0
157+
*/
158+
@SpotifyExperimentalHttpApi
159+
fun getWithNextItems(total: Int, context: CoroutineContext = Dispatchers.Default) = api.tracks.toAction { getWithNext(total, context).complete().map { it.items }.flatten() }
160+
142161
/**
143162
* Synchronously retrieve all [Z] associated with this rest action
144163
*/
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package com.adamratzman.spotify.annotations
2+
3+
@Experimental
4+
@Retention(AnnotationRetention.BINARY)
5+
@Target(AnnotationTarget.CLASS, AnnotationTarget.FUNCTION)
6+
annotation class SpotifyExperimentalHttpApi

src/commonMain/kotlin/com.adamratzman.spotify/models/PagingObjects.kt

Lines changed: 83 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ package com.adamratzman.spotify.models
33

44
import com.adamratzman.spotify.SpotifyApi
55
import com.adamratzman.spotify.SpotifyRestAction
6+
import com.adamratzman.spotify.annotations.SpotifyExperimentalHttpApi
67
import com.adamratzman.spotify.http.SpotifyEndpoint
8+
import com.adamratzman.spotify.models.PagingTraversalType.FORWARDS
79
import com.adamratzman.spotify.models.serialization.toCursorBasedPagingObject
810
import com.adamratzman.spotify.models.serialization.toPagingObject
911
import com.adamratzman.spotify.utils.runBlocking
10-
import kotlin.coroutines.CoroutineContext
11-
import kotlin.reflect.KClass
1212
import kotlinx.coroutines.Dispatchers
1313
import kotlinx.coroutines.ExperimentalCoroutinesApi
1414
import kotlinx.coroutines.flow.Flow
@@ -20,6 +20,8 @@ import kotlinx.coroutines.flow.toList
2020
import kotlinx.serialization.SerialName
2121
import kotlinx.serialization.Serializable
2222
import kotlinx.serialization.Transient
23+
import kotlin.coroutines.CoroutineContext
24+
import kotlin.reflect.KClass
2325

2426
/*
2527
Types used in PagingObjects and CursorBasedPagingObjects:
@@ -72,7 +74,7 @@ class PagingObject<T : Any>(
7274
@Suppress("UNCHECKED_CAST")
7375
override suspend fun getImpl(type: PagingTraversalType): AbstractPagingObject<T>? {
7476
val endpointFinal = endpoint!!
75-
return (if (type == PagingTraversalType.FORWARDS) next else previous)?.let { endpoint!!.get(it) }?.let { json ->
77+
return (if (type == FORWARDS) next else previous)?.let { endpoint!!.get(it) }?.let { json ->
7678
when (itemClazz) {
7779
SimpleTrack::class -> json.toPagingObject(SimpleTrack.serializer(), null, endpointFinal, endpointFinal.api.json)
7880
SpotifyCategory::class -> json.toPagingObject(SpotifyCategory.serializer(), "categories", endpointFinal, endpointFinal.api.json)
@@ -88,6 +90,19 @@ class PagingObject<T : Any>(
8890
}
8991
}
9092

93+
@SpotifyExperimentalHttpApi
94+
override suspend fun getWithNextImpl(total: Int): Sequence<AbstractPagingObject<T>> {
95+
val pagingObjects = mutableListOf<AbstractPagingObject<T>>(this)
96+
97+
var nxt = next?.let { getNext() }
98+
while (pagingObjects.size < total && nxt != null) {
99+
pagingObjects.add(nxt)
100+
nxt = nxt.next?.let { nxt?.getNext() }
101+
}
102+
103+
return pagingObjects.distinctBy { it.href }.asSequence()
104+
}
105+
91106
override suspend fun getAllImpl(): Sequence<AbstractPagingObject<T>> {
92107
val pagingObjects = mutableListOf<AbstractPagingObject<T>>()
93108
var prev = previous?.let { getPrevious() }
@@ -115,6 +130,16 @@ class PagingObject<T : Any>(
115130
@Suppress("UNCHECKED_CAST")
116131
suspend fun getAll() = endpoint!!.toAction { (getAllImpl() as Sequence<PagingObject<T>>).toList() }
117132

133+
/**
134+
* Synchronously retrieve the next [total] paging objects associated with this [PagingObject], including this [PagingObject].
135+
*
136+
* @param total The total amount of [PagingObject] to request, which includes this [PagingObject].
137+
* @since 3.0.0
138+
*/
139+
@SpotifyExperimentalHttpApi
140+
@Suppress("UNCHECKED_CAST")
141+
suspend fun getWithNext(total: Int) = endpoint!!.toAction { getWithNextImpl(total) }
142+
118143
/**
119144
* Get all items of type [T] associated with the request
120145
*/
@@ -151,38 +176,65 @@ class CursorBasedPagingObject<T : Any>(
151176
getAllImpl() as Sequence<CursorBasedPagingObject<T>>
152177
}
153178

179+
/**
180+
* Synchronously retrieve the next [total] paging objects associated with this [CursorBasedPagingObject], including this [CursorBasedPagingObject].
181+
*
182+
* @param total The total amount of [CursorBasedPagingObject] to request, which includes this [CursorBasedPagingObject].
183+
* @since 3.0.0
184+
*/
185+
@SpotifyExperimentalHttpApi
186+
@Suppress("UNCHECKED_CAST")
187+
fun getWithNext(total: Int) = endpoint!!.toAction {
188+
getWithNextImpl(total) as Sequence<CursorBasedPagingObject<T>>
189+
}
190+
154191
/**
155192
* Get all items of type [T] associated with the request
156193
*/
157194
override suspend fun getAllItems(context: CoroutineContext) = endpoint!!.toAction {
158195
getAll().suspendComplete(context).map { it.items }.flatten().asSequence()
159196
}
160197

161-
@Suppress("UNCHECKED_CAST")
162198
override suspend fun getImpl(type: PagingTraversalType): AbstractPagingObject<T>? {
163199
require(type != PagingTraversalType.BACKWARDS) { "CursorBasedPagingObjects only can go forwards" }
164-
return next?.let {
165-
val url = endpoint!!.get(it)
166-
when (itemClazz) {
167-
PlayHistory::class -> url.toCursorBasedPagingObject(
200+
return next?.let { getCursorBasedPagingObject(it) }
201+
}
202+
203+
@Suppress("UNCHECKED_CAST")
204+
private suspend fun getCursorBasedPagingObject(url: String): CursorBasedPagingObject<T>? {
205+
val json = endpoint!!.get(url)
206+
return when (itemClazz) {
207+
PlayHistory::class -> json.toCursorBasedPagingObject(
168208
PlayHistory.serializer(),
169209
null,
170210
endpoint!!,
171211
endpoint!!.api.json
172-
)
173-
Artist::class -> url.toCursorBasedPagingObject(
212+
)
213+
Artist::class -> json.toCursorBasedPagingObject(
174214
Artist.serializer(),
175215
null,
176216
endpoint!!,
177217
endpoint!!.api.json
178-
)
179-
else -> throw IllegalArgumentException("Unknown type in $href")
180-
} as? CursorBasedPagingObject<T>
181-
}
218+
)
219+
else -> throw IllegalArgumentException("Unknown type in $href")
220+
} as? CursorBasedPagingObject<T>
182221
}
183222

184223
override suspend fun getAllImpl(): Sequence<AbstractPagingObject<T>> {
185-
return generateSequence(this) { runBlocking { it.getImpl(PagingTraversalType.FORWARDS) as? CursorBasedPagingObject<T> } }
224+
return generateSequence(this) { runBlocking { it.getImpl(FORWARDS) as? CursorBasedPagingObject<T> } }
225+
}
226+
227+
@SpotifyExperimentalHttpApi
228+
override suspend fun getWithNextImpl(total: Int): Sequence<AbstractPagingObject<T>> {
229+
val pagingObjects = mutableListOf<AbstractPagingObject<T>>(this)
230+
231+
var nxt = getNext()
232+
while (pagingObjects.size < total && nxt != null) {
233+
pagingObjects.add(nxt)
234+
nxt = nxt.next?.let { nxt?.getNext() }
235+
}
236+
237+
return pagingObjects.distinctBy { it.href }.asSequence()
186238
}
187239
}
188240

@@ -223,9 +275,18 @@ abstract class AbstractPagingObject<T : Any>(
223275
internal abstract suspend fun getImpl(type: PagingTraversalType): AbstractPagingObject<T>?
224276
internal abstract suspend fun getAllImpl(): Sequence<AbstractPagingObject<T>>
225277

278+
/**
279+
* Synchronously retrieve the next [total] paging objects associated with this [AbstractPagingObject], including this [AbstractPagingObject].
280+
*
281+
* @param total The total amount of [AbstractPagingObject] to request, which includes this [AbstractPagingObject].
282+
* @since 3.0.0
283+
*/
284+
@SpotifyExperimentalHttpApi
285+
internal abstract suspend fun getWithNextImpl(total: Int): Sequence<AbstractPagingObject<T>>
286+
226287
internal abstract suspend fun getAllItems(context: CoroutineContext = Dispatchers.Default): SpotifyRestAction<Sequence<T>>
227288

228-
private suspend fun getNextImpl() = getImpl(PagingTraversalType.FORWARDS)
289+
private suspend fun getNextImpl() = getImpl(FORWARDS)
229290
private suspend fun getPreviousImpl() = getImpl(PagingTraversalType.BACKWARDS)
230291

231292
suspend fun getNext(): AbstractPagingObject<T>? = getNextImpl()
@@ -259,12 +320,12 @@ abstract class AbstractPagingObject<T : Any>(
259320

260321
@ExperimentalCoroutinesApi
261322
fun flowStartOrdered(): Flow<AbstractPagingObject<T>> =
262-
flow {
263-
if (previous == null) return@flow
264-
flowBackward().toList().reversed().also {
265-
emitAll(it.asFlow())
266-
}
267-
}.flowOn(Dispatchers.Default)
323+
flow {
324+
if (previous == null) return@flow
325+
flowBackward().toList().reversed().also {
326+
emitAll(it.asFlow())
327+
}
328+
}.flowOn(Dispatchers.Default)
268329

269330
@ExperimentalCoroutinesApi
270331
fun flowEndOrdered(): Flow<AbstractPagingObject<T>> = flowForward()

src/commonMain/kotlin/com.adamratzman.spotify/models/serialization/SerializationUtils.kt

Lines changed: 44 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import kotlinx.serialization.json.JsonElement
1515
import kotlinx.serialization.json.JsonObject
1616
import kotlinx.serialization.map
1717
import kotlinx.serialization.serializer
18+
import kotlin.reflect.KClass
1819

1920
@Suppress("EXPERIMENTAL_API_USAGE")
2021
internal val stableJson =
@@ -52,42 +53,66 @@ internal inline fun <reified T> String.toList(serializer: KSerializer<List<T>>,
5253
}
5354
}
5455

55-
internal inline fun <reified T : Any> String.toPagingObject(
56-
tSerializer: KSerializer<T>,
57-
innerObjectName: String? = null,
58-
endpoint: SpotifyEndpoint,
59-
json: Json
56+
internal fun <T : Any> String.toPagingObject(
57+
tClazz: KClass<T>,
58+
tSerializer: KSerializer<T>,
59+
innerObjectName: String? = null,
60+
endpoint: SpotifyEndpoint,
61+
json: Json,
62+
arbitraryInnerNameAllowed: Boolean = false
6063
): PagingObject<T> {
61-
if (innerObjectName != null) {
64+
if (innerObjectName != null || arbitraryInnerNameAllowed) {
6265
val map = this.parseJson { json.parse((String.serializer() to PagingObject.serializer(tSerializer)).map, this) }
63-
return (map[innerObjectName] ?: error(""))
66+
return (map[innerObjectName] ?: if (arbitraryInnerNameAllowed) map.keys.firstOrNull()?.let { map[it] }
67+
?: error("") else error(""))
6468
.apply {
6569
this.endpoint = endpoint
66-
this.itemClazz = T::class
70+
this.itemClazz = tClazz
6771
this.items.map { obj ->
6872
if (obj is NeedsApi) obj.api = endpoint.api
6973
if (obj is AbstractPagingObject<*>) obj.endpoint = endpoint
7074
}
7175
}
7276
}
7377

74-
val pagingObject = this.parseJson { json.parse(PagingObject.serializer(tSerializer), this) }
78+
return try {
79+
val pagingObject = this.parseJson { json.parse(PagingObject.serializer(tSerializer), this) }
7580

76-
return pagingObject.apply {
77-
this.endpoint = endpoint
78-
this.itemClazz = T::class
79-
this.items.map { obj ->
80-
if (obj is NeedsApi) obj.api = endpoint.api
81-
if (obj is AbstractPagingObject<*>) obj.endpoint = endpoint
81+
pagingObject.apply {
82+
this.endpoint = endpoint
83+
this.itemClazz = tClazz
84+
this.items.map { obj ->
85+
if (obj is NeedsApi) obj.api = endpoint.api
86+
if (obj is AbstractPagingObject<*>) obj.endpoint = endpoint
87+
}
8288
}
89+
} catch (jde: SpotifyException.ParseException) {
90+
if (!arbitraryInnerNameAllowed && jde.message?.contains("unable to parse", true) == true) {
91+
toPagingObject(
92+
tClazz,
93+
tSerializer,
94+
innerObjectName,
95+
endpoint,
96+
json,
97+
true
98+
)
99+
} else throw jde
83100
}
84101
}
85102

103+
internal inline fun <reified T : Any> String.toPagingObject(
104+
tSerializer: KSerializer<T>,
105+
innerObjectName: String? = null,
106+
endpoint: SpotifyEndpoint,
107+
json: Json,
108+
arbitraryInnerNameAllowed: Boolean = false
109+
): PagingObject<T> = toPagingObject(T::class, tSerializer, innerObjectName, endpoint, json, arbitraryInnerNameAllowed)
110+
86111
internal inline fun <reified T : Any> String.toCursorBasedPagingObject(
87-
tSerializer: KSerializer<T>,
88-
innerObjectName: String? = null,
89-
endpoint: SpotifyEndpoint,
90-
json: Json
112+
tSerializer: KSerializer<T>,
113+
innerObjectName: String? = null,
114+
endpoint: SpotifyEndpoint,
115+
json: Json
91116
): CursorBasedPagingObject<T> {
92117
if (innerObjectName != null) {
93118
val map = this.parseJson { json.parse((String.serializer() to CursorBasedPagingObject.serializer(tSerializer)).map, this) }

src/commonMain/kotlin/com.adamratzman.spotify/utils/Utils.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,4 +24,4 @@ internal fun <T : ResultEnum> Array<T>.match(identifier: String) =
2424

2525
internal expect fun formatDate(format: String, date: Long): String
2626

27-
internal expect fun <T> runBlocking(coroutineCode: suspend () -> T): T
27+
expect fun <T> runBlocking(coroutineCode: suspend () -> T): T

0 commit comments

Comments
 (0)