Skip to content

Commit c636f1a

Browse files
committed
Simplify ApiBuilderDsl implementation
* Added suspend building * Moved some code into common interface instead of reimplementing the same stuff * Specify the BuilderType for common SpotifyApi * Make SpotifyApi sealed * Added isTokenValid suspending function
1 parent 3b6ef36 commit c636f1a

23 files changed

+75
-92
lines changed

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

Lines changed: 30 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -7,19 +7,16 @@ import com.adamratzman.spotify.models.SpotifyAuthenticationException
77
import com.adamratzman.spotify.models.Token
88
import com.adamratzman.spotify.models.serialization.toObject
99
import com.adamratzman.spotify.utils.runBlocking
10-
import kotlinx.coroutines.Dispatchers
11-
import kotlinx.coroutines.GlobalScope
10+
import kotlinx.coroutines.CoroutineScope
1211
import kotlinx.coroutines.Job
13-
import kotlinx.coroutines.coroutineScope
1412
import kotlinx.coroutines.launch
15-
import kotlinx.coroutines.withContext
1613

1714
// Kotlin DSL builders
1815

1916
fun spotifyAppApi(block: SpotifyAppApiBuilder.() -> Unit) = SpotifyAppApiBuilder().apply(block)
2017
fun spotifyClientApi(block: SpotifyClientApiBuilder.() -> Unit) = SpotifyClientApiBuilder().apply(block)
2118

22-
/** //TODO add suspend functions
19+
/**
2320
* Spotify API builder
2421
*/
2522
class SpotifyApiBuilder(
@@ -101,7 +98,7 @@ class SpotifyApiBuilder(
10198
* Create a [SpotifyApi] instance with the given [SpotifyApiBuilder] parameters and the type -
10299
* [AuthorizationType.CLIENT] for client authentication, or otherwise [AuthorizationType.APPLICATION]
103100
*/
104-
fun build(type: AuthorizationType): SpotifyApi {
101+
fun build(type: AuthorizationType): SpotifyApi<*, *> {
105102
return if (type == AuthorizationType.CLIENT) buildClient()
106103
else buildCredentialed()
107104
}
@@ -114,7 +111,7 @@ class SpotifyApiBuilder(
114111
/**
115112
* Create a new [SpotifyAppApi] that only has access to *public* endpoints and data
116113
*/
117-
fun buildCredentialed(): SpotifyApi = spotifyAppApi {
114+
fun buildCredentialed(): SpotifyAppApi = spotifyAppApi {
118115
credentials {
119116
clientId = this@SpotifyApiBuilder.clientId
120117
clientSecret = this@SpotifyApiBuilder.clientSecret
@@ -143,7 +140,7 @@ enum class AuthorizationType {
143140
APPLICATION;
144141
}
145142

146-
interface ISpotifyApiBuilder {
143+
interface ISpotifyApiBuilder<T : SpotifyApi<T, B>, B: ISpotifyApiBuilder<T, B>> {
147144
var credentials: SpotifyCredentials
148145
var authorization: SpotifyUserAuthorization
149146
var options: SpotifyApiOptions
@@ -196,27 +193,35 @@ interface ISpotifyApiBuilder {
196193
}
197194

198195
fun options(options: SpotifyApiOptions) = apply { this.options = options }
199-
}
200196

201-
interface ISpotifyClientApiBuilder : ISpotifyApiBuilder {
202197
/**
203-
* Build the client api by providing an authorization code, token string, or token object. Only one of the following
204-
* needs to be provided
198+
* Build the [T] by provided information
199+
*/
200+
suspend fun suspendBuild(): T
201+
202+
/**
203+
* Build the [T] by provided information
204+
*/
205+
fun build(): T = runBlocking { suspendBuild() }
206+
207+
/**
208+
* Build the [T] by provided information
205209
*
206-
* @param authorizationCode Spotify authorization code retrieved after authentication
207-
* @param tokenString Spotify authorization token
208-
* @param token [Token] object (useful if you already have exchanged an authorization code yourself
210+
* Provide a consumer object to be executed after the api has been successfully built
209211
*/
210-
fun build(): SpotifyClientApi
212+
fun buildAsyncAt(scope: CoroutineScope, consumer: (T) -> Unit): Job = with(scope) { buildAsync(consumer) }
211213

212214
/**
213-
* Build the client api using a provided authorization code, token string, or token object (only one of which
214-
* is necessary)
215+
* Build the [T] by provided information
215216
*
216-
* Provide a consumer object to be executed after the client has been successfully built
217+
* Provide a consumer object to be executed after the api has been successfully built
217218
*/
218-
suspend fun buildAsync(consumer: (SpotifyClientApi) -> Unit)
219+
fun CoroutineScope.buildAsync(consumer: (T) -> Unit): Job = launch {
220+
consumer(suspendBuild())
221+
}
222+
}
219223

224+
interface ISpotifyClientApiBuilder : ISpotifyApiBuilder<SpotifyClientApi, SpotifyClientApiBuilder> {
220225
/**
221226
* Create a Spotify authorization URL from which API access can be obtained
222227
*
@@ -236,15 +241,14 @@ class SpotifyClientApiBuilder(
236241
return getAuthUrlFull(*scopes, clientId = credentials.clientId!!, redirectUri = credentials.redirectUri!!)
237242
}
238243

239-
override fun build(): SpotifyClientApi {
244+
override suspend fun suspendBuild(): SpotifyClientApi {
240245
val clientId = credentials.clientId
241246
val clientSecret = credentials.clientSecret
242247
val redirectUri = credentials.redirectUri
243248

244249
require((clientId != null && clientSecret != null && redirectUri != null) || authorization.token != null || authorization.tokenString != null) { "You need to specify a valid clientId, clientSecret, and redirectUri in the credentials block!" }
245250
return when {
246-
authorization.authorizationCode != null -> runBlocking {
247-
try {
251+
authorization.authorizationCode != null -> try {
248252
require(clientId != null && clientSecret != null && redirectUri != null) { "You need to specify a valid clientId, clientSecret, and redirectUri in the credentials block!" }
249253

250254
val response = executeTokenRequest(
@@ -278,7 +282,6 @@ class SpotifyClientApiBuilder(
278282
} catch (e: Exception) {
279283
throw SpotifyAuthenticationException("Invalid credentials provided in the login process", e)
280284
}
281-
}
282285
authorization.token != null -> SpotifyClientApi(
283286
clientId,
284287
clientSecret,
@@ -315,49 +318,19 @@ class SpotifyClientApiBuilder(
315318
)
316319
}
317320
}
318-
319-
override suspend fun buildAsync(consumer: (SpotifyClientApi) -> Unit) = coroutineScope {
320-
withContext(Dispatchers.Default) { consumer(build()) }
321-
}
322321
}
323322

324-
interface ISpotifyAppApiBuilder : ISpotifyApiBuilder {
325-
/**
326-
* Build a public [SpotifyAppApi] using the provided credentials
327-
*
328-
* @param consumer Consumer to be executed after the api has been successfully built
329-
*/
330-
fun buildAsync(consumer: (SpotifyApi) -> Unit): Job
331-
332-
/**
333-
* Build a public [SpotifyAppApi] using the provided credentials
334-
*/
335-
fun build(): SpotifyApi
336-
}
323+
interface ISpotifyAppApiBuilder : ISpotifyApiBuilder<SpotifyAppApi, SpotifyAppApiBuilder>
337324

338325
class SpotifyAppApiBuilder(
339326
override var credentials: SpotifyCredentials = SpotifyCredentialsBuilder().build(),
340327
override var authorization: SpotifyUserAuthorization = SpotifyUserAuthorizationBuilder().build(),
341328
override var options: SpotifyApiOptions = SpotifyApiOptionsBuilder().build()
342329
) : ISpotifyAppApiBuilder {
343-
/**
344-
* Create a new [SpotifyAppApi] that only has access to *public* endpoints and data
345-
*/
346-
fun buildPublic() = build()
347-
348-
/**
349-
* Build a public [SpotifyAppApi] using the provided credentials
350-
*
351-
* Provide a consumer object to be executed after the api has been successfully built
352-
*/
353-
override fun buildAsync(consumer: (SpotifyApi) -> Unit) = GlobalScope.launch {
354-
withContext(Dispatchers.Default) { consumer(build()) }
355-
}
356-
357330
/**
358331
* Build a public [SpotifyAppApi] using the provided credentials
359332
*/
360-
override fun build(): SpotifyApi {
333+
override suspend fun suspendBuild(): SpotifyAppApi {
361334
val clientId = credentials.clientId
362335
val clientSecret = credentials.clientSecret
363336
require((clientId != null && clientSecret != null) || authorization.token != null || authorization.tokenString != null) { "You didn't specify a client id or client secret in the credentials block!" }
@@ -393,7 +366,7 @@ class SpotifyAppApiBuilder(
393366
}
394367
else -> try {
395368
require(clientId != null && clientSecret != null) { "Illegal credentials provided" }
396-
val token = runBlocking { getCredentialedToken(clientId, clientSecret, null) }
369+
val token = getCredentialedToken(clientId, clientSecret, null)
397370
SpotifyAppApi(
398371
clientId,
399372
clientSecret,

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

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,9 @@ import com.adamratzman.spotify.models.TokenValidityResponse
2828
import com.adamratzman.spotify.models.serialization.toObject
2929
import com.adamratzman.spotify.utils.runBlocking
3030
import com.adamratzman.spotify.utils.toList
31+
import kotlinx.coroutines.Dispatchers
32+
import kotlin.coroutines.CoroutineContext
33+
import kotlin.jvm.JvmOverloads
3134

3235
internal const val base = "https://api.spotify.com/v1"
3336

@@ -52,7 +55,7 @@ internal const val base = "https://api.spotify.com/v1"
5255
* @property logger The Spotify event logger
5356
*
5457
*/
55-
abstract class SpotifyApi internal constructor(
58+
sealed class SpotifyApi<T: SpotifyApi<T, B>, B : ISpotifyApiBuilder<T, B>>(
5659
val clientId: String?,
5760
val clientSecret: String?,
5861
var token: Token,
@@ -126,7 +129,7 @@ abstract class SpotifyApi internal constructor(
126129
/**
127130
* Return a new [SpotifyApiBuilderDsl] with the parameters provided to this api instance
128131
*/
129-
abstract fun getApiBuilderDsl(): ISpotifyApiBuilder
132+
abstract fun getApiBuilderDsl(): B
130133

131134
private fun clearCaches(vararg endpoints: SpotifyEndpoint) {
132135
endpoints.forEach { it.cache.clear() }
@@ -155,23 +158,29 @@ abstract class SpotifyApi internal constructor(
155158
return getAuthUrlFull(*scopes, clientId = clientId, redirectUri = redirectUri)
156159
}
157160

158-
/** //TODO add suspend version of it
161+
/**
159162
* Tests whether the current [token] is actually valid. By default, an endpoint is called *once* to verify
160163
* validity.
161164
*
162165
* @param makeTestRequest Whether to also make an endpoint request to verify authentication.
163166
*
164167
* @return [TokenValidityResponse] containing whether this token is valid, and if not, an Exception explaining why
165168
*/
166-
fun isTokenValid(makeTestRequest: Boolean = true): TokenValidityResponse {
169+
@JvmOverloads
170+
fun isTokenValid(makeTestRequest: Boolean = true): TokenValidityResponse = runBlocking {
171+
suspendIsTokenValid(makeTestRequest)
172+
}
173+
174+
@JvmOverloads
175+
suspend fun suspendIsTokenValid(makeTestRequest: Boolean = true, context: CoroutineContext = Dispatchers.Default): TokenValidityResponse {
167176
if (token.shouldRefresh()) return TokenValidityResponse(
168177
false,
169178
SpotifyAuthenticationException("Token needs to be refreshed (is it expired?)")
170179
)
171180
if (!makeTestRequest) return TokenValidityResponse(true, null)
172181

173182
return try {
174-
browse.getAvailableGenreSeeds().complete()
183+
browse.getAvailableGenreSeeds().suspendComplete(context)
175184
TokenValidityResponse(true, null)
176185
} catch (e: Exception) {
177186
TokenValidityResponse(false, e)
@@ -202,7 +211,7 @@ class SpotifyAppApi internal constructor(
202211
retryWhenRateLimited: Boolean,
203212
enableLogger: Boolean,
204213
testTokenValidity: Boolean
205-
) : SpotifyApi(
214+
) : SpotifyApi<SpotifyAppApi, SpotifyAppApiBuilder>(
206215
clientId,
207216
clientSecret,
208217
token,
@@ -303,7 +312,7 @@ class SpotifyClientApi internal constructor(
303312
retryWhenRateLimited: Boolean,
304313
enableLogger: Boolean,
305314
testTokenValidity: Boolean
306-
) : SpotifyApi(
315+
) : SpotifyApi<SpotifyClientApi, SpotifyClientApiBuilder>(
307316
clientId,
308317
clientSecret,
309318
token,
@@ -500,7 +509,7 @@ class SpotifyClientApi internal constructor(
500509
scopes.all { token.scopes.contains(it) }
501510
}
502511

503-
typealias SpotifyAPI = SpotifyApi
512+
typealias SpotifyAPI<T, B> = SpotifyApi<T, B>
504513
typealias SpotifyClientAPI = SpotifyClientApi
505514
typealias SpotifyAppAPI = SpotifyAppApi
506515

@@ -511,7 +520,7 @@ fun getAuthUrlFull(vararg scopes: SpotifyScope, clientId: String, redirectUri: S
511520
if (scopes.isEmpty()) "" else "&scope=${scopes.joinToString("%20") { it.uri }}"
512521
}
513522

514-
suspend fun getCredentialedToken(clientId: String, clientSecret: String, api: SpotifyApi?): Token {
523+
suspend fun getCredentialedToken(clientId: String, clientSecret: String, api: SpotifyApi<*, *>?): Token {
515524
val response = executeTokenRequest(
516525
HttpConnection(
517526
"https://accounts.spotify.com/api/token",

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ import kotlin.jvm.JvmOverloads
2828
/**
2929
* Provides a uniform interface to retrieve, whether synchronously or asynchronously, [T] from Spotify
3030
*/
31-
open class SpotifyRestAction<T> internal constructor(protected val api: SpotifyApi, val supplier: suspend () -> T) {
31+
open class SpotifyRestAction<T> internal constructor(protected val api: SpotifyApi<*, *>, val supplier: suspend () -> T) {
3232
private var hasRunBacking: Boolean = false
3333
private var hasCompletedBacking: Boolean = false
3434

@@ -126,7 +126,7 @@ open class SpotifyRestAction<T> internal constructor(protected val api: SpotifyA
126126
override fun toString() = complete().toString()
127127
}
128128

129-
class SpotifyRestActionPaging<Z : Any, T : AbstractPagingObject<Z>>(api: SpotifyApi, supplier: suspend () -> T) :
129+
class SpotifyRestActionPaging<Z : Any, T : AbstractPagingObject<Z>>(api: SpotifyApi<*, *>, supplier: suspend () -> T) :
130130
SpotifyRestAction<T>(api, supplier) {
131131

132132
/**

src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientFollowingApi.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ typealias ClientFollowingAPI = ClientFollowingApi
2424
/**
2525
* These endpoints allow you manage the artists, users and playlists that a Spotify user follows.
2626
*/
27-
class ClientFollowingApi(api: SpotifyApi) : FollowingApi(api) {
27+
class ClientFollowingApi(api: SpotifyApi<*, *>) : FollowingApi(api) {
2828
/**
2929
* Check to see if the current user is following another Spotify user.
3030
*

src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientLibraryApi.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ typealias ClientLibraryAPI = ClientLibraryApi
2424
/**
2525
* Endpoints for retrieving information about, and managing, tracks that the current user has saved in their “Your Music” library.
2626
*/
27-
class ClientLibraryApi(api: SpotifyApi) : SpotifyEndpoint(api) {
27+
class ClientLibraryApi(api: SpotifyApi<*, *>) : SpotifyEndpoint(api) {
2828
/**
2929
* Get a list of the songs saved in the current Spotify user’s ‘Your Music’ library.
3030
*

src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientPersonalizationApi.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ typealias ClientPersonalizationAPI = ClientPersonalizationApi
1616
/**
1717
* Endpoints for retrieving information about the user’s listening habits.
1818
*/
19-
class ClientPersonalizationApi(api: SpotifyApi) : SpotifyEndpoint(api) {
19+
class ClientPersonalizationApi(api: SpotifyApi<*, *>) : SpotifyEndpoint(api) {
2020
/**
2121
* The time frame for which attribute affinities are computed.
2222
*

src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientPlayerApi.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ typealias ClientPlayerAPI = ClientPlayerApi
3434
* These endpoints allow for viewing and controlling user playback. Please view [the official documentation](https://developer.spotify.com/web-api/working-with-connect/)
3535
* for more information on how this works. This is in beta and is available for **premium users only**. Endpoints are **not** guaranteed to work
3636
*/
37-
class ClientPlayerApi(api: SpotifyApi) : SpotifyEndpoint(api) {
37+
class ClientPlayerApi(api: SpotifyApi<*, *>) : SpotifyEndpoint(api) {
3838
/**
3939
* Get information about a user’s available devices.
4040
*

src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientPlaylistApi.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ typealias ClientPlaylistAPI = ClientPlaylistApi
3939
/**
4040
* Endpoints for retrieving information about a user’s playlists and for managing a user’s playlists.
4141
*/
42-
class ClientPlaylistApi(api: SpotifyApi) : PlaylistApi(api) {
42+
class ClientPlaylistApi(api: SpotifyApi<*, *>) : PlaylistApi(api) {
4343
/**
4444
* Create a playlist for a Spotify user. (The playlist will be empty until you add tracks.)
4545
*

src/commonMain/kotlin/com.adamratzman.spotify/endpoints/client/ClientProfileApi.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ typealias ClientUserAPI = ClientProfileApi
1414
/**
1515
* Endpoints for retrieving information about a user’s profile.
1616
*/
17-
class ClientProfileApi(api: SpotifyApi) : UserApi(api) {
17+
class ClientProfileApi(api: SpotifyApi<*, *>) : UserApi(api) {
1818
/**
1919
* Get detailed profile information about the current user (including the current user’s username).
2020
*

src/commonMain/kotlin/com.adamratzman.spotify/endpoints/public/AlbumApi.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ typealias AlbumAPI = AlbumApi
2222
/**
2323
* Endpoints for retrieving information about one or more albums from the Spotify catalog.
2424
*/
25-
class AlbumApi(api: SpotifyApi) : SpotifyEndpoint(api) {
25+
class AlbumApi(api: SpotifyApi<*, *>) : SpotifyEndpoint(api) {
2626
/**
2727
* Get Spotify catalog information for a single album.
2828
*

0 commit comments

Comments
 (0)