Simplify MealieDataSourceImpl

This commit is contained in:
Kirill Kamakin
2022-08-07 11:42:36 +02:00
parent 35566d8fa9
commit 725b75211d
4 changed files with 49 additions and 86 deletions

View File

@@ -2,7 +2,6 @@ package gq.kirmanak.mealient.data.auth.impl
import gq.kirmanak.mealient.data.auth.AuthDataSource
import gq.kirmanak.mealient.datasource.MealieDataSource
import gq.kirmanak.mealient.extensions.logAndMapErrors
import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject
import javax.inject.Singleton
@@ -13,13 +12,6 @@ class AuthDataSourceImpl @Inject constructor(
private val mealieDataSource: MealieDataSource,
) : AuthDataSource {
override suspend fun authenticate(username: String, password: String, baseUrl: String): String {
logger.v { "authenticate() called with: username = $username, password = $password" }
val accessToken = logger.logAndMapErrors(
block = { mealieDataSource.authenticate(baseUrl, username, password) },
logProvider = { "sendRequest: can't get token" },
)
logger.v { "authenticate() returned: $accessToken" }
return accessToken
}
override suspend fun authenticate(username: String, password: String, baseUrl: String): String =
mealieDataSource.authenticate(baseUrl, username, password)
}

View File

@@ -1,12 +0,0 @@
package gq.kirmanak.mealient.extensions
import gq.kirmanak.mealient.datasource.mapToNetworkError
import gq.kirmanak.mealient.logging.Logger
inline fun <T> Logger.logAndMapErrors(
block: () -> T,
noinline logProvider: () -> String
): T = runCatchingExceptCancel(block).getOrElse {
e(it, messageSupplier = logProvider)
throw it.mapToNetworkError()
}

View File

@@ -6,8 +6,8 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import okhttp3.ResponseBody
import retrofit2.HttpException
import retrofit2.Response
import javax.inject.Inject
import javax.inject.Singleton
@@ -20,70 +20,65 @@ class MealieDataSourceImpl @Inject constructor(
override suspend fun addRecipe(
baseUrl: String, token: String?, recipe: AddRecipeRequest
): String {
logger.v { "addRecipe() called with: baseUrl = $baseUrl, token = $token, recipe = $recipe" }
return mealieService.runCatching { addRecipe("$baseUrl/api/recipes/create", token, recipe) }
.onFailure { logger.e(it) { "addRecipe() request failed with: baseUrl = $baseUrl, token = $token, recipe = $recipe" } }
.onSuccess { logger.d { "addRecipe() request succeeded with: baseUrl = $baseUrl, token = $token, recipe = $recipe" } }
.getOrThrowUnauthorized()
}
): String = makeCall(
block = { addRecipe("$baseUrl/api/recipes/create", token, recipe) },
logMethod = { "addRecipe" },
logParameters = { "baseUrl = $baseUrl, token = $token, recipe = $recipe" }
).getOrThrowUnauthorized()
override suspend fun authenticate(
baseUrl: String, username: String, password: String
): String {
logger.v { "authenticate() called with: baseUrl = $baseUrl, username = $username, password = $password" }
return mealieService.runCatching { getToken("$baseUrl/api/auth/token", username, password) }
.onFailure { logger.e(it) { "authenticate() request failed with: baseUrl = $baseUrl, username = $username, password = $password" } }
.onSuccess { logger.d { "authenticate() request succeeded with: baseUrl = $baseUrl, username = $username, password = $password" } }
.mapCatching { parseToken(it) }
.getOrThrowUnauthorized()
): String = makeCall(
block = { getToken("$baseUrl/api/auth/token", username, password) },
logMethod = { "authenticate" },
logParameters = { "baseUrl = $baseUrl, username = $username, password = $password" }
).map { it.accessToken }.getOrElse {
val errorBody = (it as? HttpException)?.response()?.errorBody() ?: throw it
val errorDetail = errorBody.decode<ErrorDetail>()
throw if (errorDetail.detail == "Unauthorized") NetworkError.Unauthorized(it) else it
}
override suspend fun getVersionInfo(baseUrl: String): VersionResponse {
logger.v { "getVersionInfo() called with: baseUrl = $baseUrl" }
return mealieService.runCatching { getVersion("$baseUrl/api/debug/version") }
.onFailure { logger.e(it) { "getVersionInfo() request failed with: baseUrl = $baseUrl" } }
.onSuccess { logger.d { "getVersionInfo() request succeeded with: baseUrl = $baseUrl" } }
.getOrThrowUnauthorized()
override suspend fun getVersionInfo(baseUrl: String): VersionResponse = makeCall(
block = { getVersion("$baseUrl/api/debug/version") },
logMethod = { "getVersionInfo" },
logParameters = { "baseUrl = $baseUrl" },
).getOrElse {
throw when (it) {
is HttpException, is SerializationException -> NetworkError.NotMealie(it)
else -> NetworkError.MalformedUrl(it)
}
}
override suspend fun requestRecipes(
baseUrl: String, token: String?, start: Int, limit: Int
): List<GetRecipeSummaryResponse> {
logger.v { "requestRecipes() called with: baseUrl = $baseUrl, token = $token, start = $start, limit = $limit" }
return mealieService.runCatching {
getRecipeSummary("$baseUrl/api/recipes/summary", token, start, limit)
}
.onFailure { logger.e(it) { "requestRecipes() request failed with: baseUrl = $baseUrl, token = $token, start = $start, limit = $limit" } }
.onSuccess { logger.d { "requestRecipes() request succeeded with: baseUrl = $baseUrl, token = $token, start = $start, limit = $limit" } }
.getOrThrowUnauthorized()
}
): List<GetRecipeSummaryResponse> =
makeCall(
block = { getRecipeSummary("$baseUrl/api/recipes/summary", token, start, limit) },
logMethod = { "requestRecipes" },
logParameters = { "baseUrl = $baseUrl, token = $token, start = $start, limit = $limit" }
).getOrThrowUnauthorized()
override suspend fun requestRecipeInfo(
baseUrl: String, token: String?, slug: String
): GetRecipeResponse {
logger.v { "requestRecipeInfo() called with: baseUrl = $baseUrl, token = $token, slug = $slug" }
return mealieService.runCatching { getRecipe("$baseUrl/api/recipes/$slug", token) }
.onFailure { logger.e(it) { "requestRecipeInfo() request failed with: baseUrl = $baseUrl, token = $token, slug = $slug" } }
.onSuccess { logger.d { "requestRecipeInfo() request succeeded with: baseUrl = $baseUrl, token = $token, slug = $slug" } }
.getOrThrowUnauthorized()
): GetRecipeResponse = makeCall(
block = { getRecipe("$baseUrl/api/recipes/$slug", token) },
logMethod = { "requestRecipeInfo" },
logParameters = { "baseUrl = $baseUrl, token = $token, slug = $slug" }
).getOrThrowUnauthorized()
private suspend inline fun <T> makeCall(
crossinline block: suspend MealieService.() -> T,
crossinline logMethod: () -> String,
crossinline logParameters: () -> String,
): Result<T> {
logger.v { "${logMethod()} called with: ${logParameters()}" }
return mealieService.runCatching { block() }
.onFailure { logger.e(it) { "${logMethod()} request failed with: ${logParameters()}" } }
.onSuccess { logger.d { "${logMethod()} request succeeded with ${logParameters()}" } }
}
private fun parseToken(
response: Response<GetTokenResponse>
): String = if (response.isSuccessful) {
response.body()?.accessToken
?: throw NetworkError.NotMealie(NullPointerException("Body is null"))
} else {
val cause = HttpException(response)
val errorDetail = json.runCatching<Json, ErrorDetail> { decodeErrorBody(response) }
.onFailure { logger.e(it) { "Can't decode error body" } }
.getOrNull()
throw when (errorDetail?.detail) {
"Unauthorized" -> NetworkError.Unauthorized(cause)
else -> NetworkError.NotMealie(cause)
}
}
@OptIn(ExperimentalSerializationApi::class)
private inline fun <reified R> ResponseBody.decode(): R = json.decodeFromStream(byteStream())
}
private fun <T> Result<T>.getOrThrowUnauthorized(): T = getOrElse {
@@ -92,15 +87,4 @@ private fun <T> Result<T>.getOrThrowUnauthorized(): T = getOrElse {
} else {
it
}
}
@OptIn(ExperimentalSerializationApi::class)
private inline fun <T, reified R> Json.decodeErrorBody(response: Response<T>): R {
val responseBody = checkNotNull(response.errorBody()) { "Can't decode absent error body" }
return decodeFromStream(responseBody.byteStream())
}
fun Throwable.mapToNetworkError(): NetworkError = when (this) {
is HttpException, is SerializationException -> NetworkError.NotMealie(this)
else -> NetworkError.NoServerConnection(this)
}

View File

@@ -2,7 +2,6 @@ package gq.kirmanak.mealient.datasource
import gq.kirmanak.mealient.datasource.DataSourceModule.Companion.AUTHORIZATION_HEADER_NAME
import gq.kirmanak.mealient.datasource.models.*
import retrofit2.Response
import retrofit2.http.*
interface MealieService {
@@ -13,7 +12,7 @@ interface MealieService {
@Url url: String,
@Field("username") username: String,
@Field("password") password: String,
): Response<GetTokenResponse>
): GetTokenResponse
@POST
suspend fun addRecipe(