Skip to content

Commit

Permalink
Improve the customizable HttpClient configuration
Browse files Browse the repository at this point in the history
ChrisKruegerDev committed Mar 5, 2023

Verified

This commit was signed with the committer’s verified signature. The key has expired.
universalmind303 Cory Grinstead
1 parent 817b551 commit ae8131a
Showing 12 changed files with 315 additions and 201 deletions.
132 changes: 84 additions & 48 deletions tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/Tmdb3.kt
Original file line number Diff line number Diff line change
@@ -1,53 +1,89 @@
package app.moviebase.tmdb

import app.moviebase.tmdb.api.*
import app.moviebase.tmdb.remote.TmdbHttpClientFactory
import app.moviebase.tmdb.remote.TmdbLogLevel
import app.moviebase.tmdb.remote.TmdbSessionProvider
import io.ktor.client.*

class Tmdb3(
tmdbApiKey: String,
logLevel: TmdbLogLevel = TmdbLogLevel.NONE,
tmdbSessionProvider: TmdbSessionProvider? = null,
httpClientConfigBlock: (HttpClientConfig<*>.() -> Unit)? = null,
) {
private val client: HttpClient = TmdbHttpClientFactory.create(
tmdbApiKey = tmdbApiKey,
logLevel = logLevel,
httpClientConfigBlock = httpClientConfigBlock,
)
private val clientWithSession: HttpClient = TmdbHttpClientFactory.createWithSession(
tmdbApiKey = tmdbApiKey,
logLevel = logLevel,
tmdbSessionProvider = tmdbSessionProvider,
httpClientConfigBlock = httpClientConfigBlock,
)

val account = TmdbAccountApi(clientWithSession)
val authentication = TmdbAuthenticationApi(client)
val certifications = TmdbCertificationsApi(client)
val changes = TmdbChangesApi(client)
val collections = TmdbCollectionsApi(client)
val companies = TmdbCompaniesApi(client)
val configuration = TmdbConfigurationApi(client)
val credits = TmdbCreditsApi(client)
val discover = TmdbDiscoverApi(client)
val find = TmdbFindApi(client)
val genres = TmdbGenresApi(client)
val guestSessions = TmdbGuestSessionsApi(client)
val keywords = TmdbKeywordsApi(client)
val lists = TmdbListsApi(client)
val movies = TmdbMoviesApi(client)
val networks = TmdbNetworksApi(client)
val trending = TmdbTrendingApi(client)
val people = TmdbPeopleApi(client)
val reviews = TmdbReviewsApi(client)
val search = TmdbSearchApi(client)
val show = TmdbShowApi(client)
val showSeasons = TmdbShowSeasonsApi(client)
val showEpisodes = TmdbShowEpisodesApi(client)
val showEpisodeGroups = TmdbShowEpisodeGroupsApi(client)
import app.moviebase.tmdb.api.TmdbAccountApi
import app.moviebase.tmdb.api.TmdbAuthenticationApi
import app.moviebase.tmdb.api.TmdbCertificationsApi
import app.moviebase.tmdb.api.TmdbChangesApi
import app.moviebase.tmdb.api.TmdbCollectionsApi
import app.moviebase.tmdb.api.TmdbCompaniesApi
import app.moviebase.tmdb.api.TmdbConfigurationApi
import app.moviebase.tmdb.api.TmdbCreditsApi
import app.moviebase.tmdb.api.TmdbDiscoverApi
import app.moviebase.tmdb.api.TmdbFindApi
import app.moviebase.tmdb.api.TmdbGenresApi
import app.moviebase.tmdb.api.TmdbGuestSessionsApi
import app.moviebase.tmdb.api.TmdbKeywordsApi
import app.moviebase.tmdb.api.TmdbListsApi
import app.moviebase.tmdb.api.TmdbMoviesApi
import app.moviebase.tmdb.api.TmdbNetworksApi
import app.moviebase.tmdb.api.TmdbPeopleApi
import app.moviebase.tmdb.api.TmdbReviewsApi
import app.moviebase.tmdb.api.TmdbSearchApi
import app.moviebase.tmdb.api.TmdbShowApi
import app.moviebase.tmdb.api.TmdbShowEpisodeGroupsApi
import app.moviebase.tmdb.api.TmdbShowEpisodesApi
import app.moviebase.tmdb.api.TmdbShowSeasonsApi
import app.moviebase.tmdb.api.TmdbTrendingApi
import app.moviebase.tmdb.remote.HttpClientFactory
import app.moviebase.tmdb.remote.TmdbDsl
import app.moviebase.tmdb.remote.interceptRequest
import io.ktor.client.HttpClient
import io.ktor.client.request.*

@TmdbDsl
fun Tmdb3(block: TmdbClientConfig.() -> Unit): Tmdb3 {
val config = TmdbClientConfig().apply(block)
return Tmdb3(config)
}

class Tmdb3 internal constructor(private val config: TmdbClientConfig) {

constructor(tmdbApiKey: String) : this(TmdbClientConfig.buildDefault(tmdbApiKey))

init {
requireNotNull(config.tmdbApiKey) {
"TMDB API key unavailable. Set the tmdbApiKey field in the class TmdbClientConfig when instantiate the TMDB client."
}
}

private val client: HttpClient by lazy {
HttpClientFactory.buildHttpClient(TmdbVersion.V3, config).apply {
interceptRequest {
it.parameter(TmdbUrlParameter.API_KEY, config.tmdbApiKey)

config.tmdbCredentials?.sessionIdProvider?.invoke()?.let { sessionId ->
it.parameter(TmdbUrlParameter.SESSION_ID, sessionId)
}
}
}
}

val account: TmdbAccountApi by buildApi(::TmdbAccountApi)
val authentication by buildApi(::TmdbAuthenticationApi)
val certifications by buildApi(::TmdbCertificationsApi)
val changes by buildApi(::TmdbChangesApi)
val collections by buildApi(::TmdbCollectionsApi)
val companies by buildApi(::TmdbCompaniesApi)
val configuration by buildApi(::TmdbConfigurationApi)
val credits by buildApi(::TmdbCreditsApi)
val discover by buildApi(::TmdbDiscoverApi)
val find by buildApi(::TmdbFindApi)
val genres by buildApi(::TmdbGenresApi)
val guestSessions by buildApi(::TmdbGuestSessionsApi)
val keywords by buildApi(::TmdbKeywordsApi)
val lists by buildApi(::TmdbListsApi)
val movies by buildApi(::TmdbMoviesApi)
val networks by buildApi(::TmdbNetworksApi)
val trending by buildApi(::TmdbTrendingApi)
val people by buildApi(::TmdbPeopleApi)
val reviews by buildApi(::TmdbReviewsApi)
val search by buildApi(::TmdbSearchApi)
val show by buildApi(::TmdbShowApi)
val showSeasons by buildApi(::TmdbShowSeasonsApi)
val showEpisodes by buildApi(::TmdbShowEpisodesApi)
val showEpisodeGroups by buildApi(::TmdbShowEpisodeGroupsApi)

private inline fun <T> buildApi(crossinline builder: (HttpClient) -> T) = lazy {
builder(client)
}
}
64 changes: 41 additions & 23 deletions tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/Tmdb4.kt
Original file line number Diff line number Diff line change
@@ -3,42 +3,60 @@ package app.moviebase.tmdb
import app.moviebase.tmdb.api.Tmdb4AccountApi
import app.moviebase.tmdb.api.Tmdb4AuthenticationApi
import app.moviebase.tmdb.api.Tmdb4ListApi
import app.moviebase.tmdb.remote.TmdbLogLevel
import app.moviebase.tmdb.remote.buildHttpClient
import app.moviebase.tmdb.remote.HttpClientFactory
import app.moviebase.tmdb.remote.TmdbDsl
import app.moviebase.tmdb.remote.interceptRequest
import io.ktor.client.*
import io.ktor.client.request.*

class Tmdb4(
tmdbApiKey: String,
authenticationToken: String? = null,
logLevel: TmdbLogLevel = TmdbLogLevel.NONE,
httpClientConfigBlock: (HttpClientConfig<*>.() -> Unit)? = null,
@TmdbDsl
fun Tmdb4(block: TmdbClientConfig.() -> Unit): Tmdb4 {
val config = TmdbClientConfig().apply(block)
return Tmdb4(config)
}

class Tmdb4 internal constructor(
private val config: TmdbClientConfig
) {

var accessToken: String? = null
var requestToken: String? = null
constructor(
tmdbApiKey: String,
authenticationToken: String? = null
) : this(TmdbClientConfig.buildDefault(tmdbApiKey, authenticationToken))

init {
check(!config.tmdbApiKey.isNullOrBlank()) {
"TMDB API key is unavailable. Set the tmdbApiKey when instantiate the TMDB client."
}
}

private val client by lazy {
HttpClientFactory.buildHttpClient(TmdbVersion.V4, config).apply {
interceptRequest {
it.parameter(TmdbUrlParameter.API_KEY, config.tmdbApiKey)

private val client = buildHttpClient(logLevel, httpClientConfigBlock).apply {
interceptRequest {
it.parameter(TmdbUrlParameter.API_KEY, tmdbApiKey)
accessToken?.let { token ->
it.header("Authorization", "Bearer $token")
config.tmdbCredentials?.accessTokenProvider?.let { token ->
it.header("Authorization", "Bearer $token")
}
}
}
}

private val authClient = buildHttpClient(logLevel, httpClientConfigBlock).apply {
interceptRequest {
// TODO: Install Auth client https://ktor.io/docs/auth.html
it.parameter(TmdbUrlParameter.API_KEY, tmdbApiKey)
requireNotNull(authenticationToken) { "authentication token not set for request auth endpoints" }
it.header("Authorization", "Bearer $authenticationToken")
private val authClient by lazy {
HttpClientFactory.buildHttpClient(TmdbVersion.V4, config).apply {
interceptRequest {
it.parameter(TmdbUrlParameter.API_KEY, config.tmdbApiKey)

config.tmdbAuthenticationToken?.let { token ->
it.header("Authorization", "Bearer $token")
}
}
}
}

val account = Tmdb4AccountApi(client)
val auth = Tmdb4AuthenticationApi(authClient)
val list = Tmdb4ListApi(client)
val account by buildApi(::Tmdb4AccountApi)
val auth by lazy { Tmdb4AuthenticationApi(authClient) }
val list by buildApi(::Tmdb4ListApi)

private inline fun <T> buildApi(crossinline builder: (HttpClient) -> T) = lazy { builder(client) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
package app.moviebase.tmdb

import app.moviebase.tmdb.remote.TmdbDsl
import io.ktor.client.*
import io.ktor.client.engine.*
import io.ktor.client.plugins.logging.*

@TmdbDsl
class TmdbClientConfig {

var tmdbApiKey: String? = null
var tmdbAuthenticationToken: String? = null
internal var tmdbCredentials: TmdbCredentials? = null

var expectSuccess: Boolean = true
var useCache: Boolean = false
var useTimeout: Boolean = false

internal var httpClientConfigBlock: (HttpClientConfig<*>.() -> Unit)? = null
internal var httpClientBuilder: (() -> HttpClient)? = null
internal var httpClientLoggingBlock: (Logging.Config.() -> Unit)? = null

fun tmdbAccountCredentials(block: TmdbCredentials.() -> Unit) {
tmdbCredentials = TmdbCredentials().apply(block)
}

fun logging(block: Logging.Config.() -> Unit) {
httpClientLoggingBlock = block
}

/**
* Set custom HttpClient configuration for the default HttpClient.
*/
fun httpClient(block: HttpClientConfig<*>.() -> Unit) {
this.httpClientConfigBlock = block
}

/**
* Creates an custom [HttpClient] with the specified [HttpClientEngineFactory] and optional [block] configuration.
* Note that the TMDB config will be added afterwards.
*/
fun <T : HttpClientEngineConfig> httpClient(
engineFactory: HttpClientEngineFactory<T>,
block: HttpClientConfig<T>.() -> Unit = {}
) {
httpClientBuilder = {
HttpClient(engineFactory, block)
}
}

companion object {

internal fun buildDefault(
tmdbApiKey: String,
tmdbAuthenticationToken: String? = null
) = TmdbClientConfig().apply {
this.tmdbApiKey = tmdbApiKey
this.tmdbAuthenticationToken = tmdbAuthenticationToken
}
}
}

@TmdbDsl
class TmdbCredentials {

internal var sessionIdProvider: (() -> String?)? = null
internal var accessTokenProvider: (() -> String?)? = null
internal var requestTokenProvider: (() -> String?)? = null

fun sessionId(provider: () -> String?) {
sessionIdProvider = provider
}

fun accessToken(provider: () -> String?) {
accessTokenProvider = provider
}

fun requestToken(provider: () -> String?) {
requestTokenProvider = provider
}
}
16 changes: 10 additions & 6 deletions tmdb-api/src/commonMain/kotlin/app/moviebase/tmdb/TmdbWebConfig.kt
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
package app.moviebase.tmdb

object TmdbWebConfig {

internal object TmdbWebConfig {
const val BASE_URL_TMDB = "https://api.themoviedb.org"
const val WEBSITE_BASE_URL = "https://www.themoviedb.org"

const val VERSION_PATH_V3 = "3"
const val VERSION_PATH_V4 = "4"
const val BASE_URL_TMDB_IMAGE = "https://image.tmdb.org/t/p/"
const val BASE_URL_YOUTUBE_IMAGE = "https://img.youtube.com/vi"
const val LOGO_FILTER = "_filter(negate,000,666)"

const val VERSION_PATH_V3 = "3"
const val VERSION_PATH_V4 = "4"
const val LOGO_FILTER = "_filter(negate,000,666)"
}

object TmdbUrlParameter {
internal object TmdbUrlParameter {
const val API_KEY = "api_key"
const val SESSION_ID = "session_id"
const val ACCESS_TOKEN = "access_token"
}

internal enum class TmdbVersion(val path: String) {
V3(TmdbWebConfig.VERSION_PATH_V3),
V4(TmdbWebConfig.VERSION_PATH_V4)
}
Original file line number Diff line number Diff line change
@@ -9,12 +9,12 @@ import io.ktor.util.pipeline.*
typealias RequestInterceptor = suspend (HttpRequestBuilder) -> Unit
typealias ResponseInterceptor = suspend (HttpClientCall) -> Unit

fun HttpClient.interceptRequest(phase: PipelinePhase = HttpRequestPipeline.Render, interceptor: RequestInterceptor) =
internal fun HttpClient.interceptRequest(phase: PipelinePhase = HttpRequestPipeline.Render, interceptor: RequestInterceptor) =
requestPipeline.intercept(phase) { interceptor(context) }

/**
* Interceptor for throwing an exception must run before [HttpResponsePipeline.Transform] phase.
*/
fun HttpClient.interceptResponse(phase: PipelinePhase = HttpResponsePipeline.Parse, interceptor: ResponseInterceptor) =
internal fun HttpClient.interceptResponse(phase: PipelinePhase = HttpResponsePipeline.Parse, interceptor: ResponseInterceptor) =
responsePipeline.intercept(phase) { interceptor(context) }

Loading

0 comments on commit ae8131a

Please sign in to comment.