Allow viewing recipes on V1

This commit is contained in:
Kirill Kamakin
2022-10-29 16:40:19 +02:00
parent 90eeb155b2
commit 1f5234d123
15 changed files with 178 additions and 95 deletions

View File

@@ -9,8 +9,10 @@ import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.datasource.MealieDataSource import gq.kirmanak.mealient.datasource.MealieDataSource
import gq.kirmanak.mealient.datasource.models.AddRecipeRequest import gq.kirmanak.mealient.datasource.models.AddRecipeRequest
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
import gq.kirmanak.mealient.datasource.models.NetworkError import gq.kirmanak.mealient.datasource.models.NetworkError
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import gq.kirmanak.mealient.extensions.toVersionInfo import gq.kirmanak.mealient.extensions.toVersionInfo
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -20,28 +22,52 @@ class MealieDataSourceWrapper @Inject constructor(
private val baseURLStorage: BaseURLStorage, private val baseURLStorage: BaseURLStorage,
private val authRepo: AuthRepo, private val authRepo: AuthRepo,
private val mealieDataSource: MealieDataSource, private val mealieDataSource: MealieDataSource,
private val mealieDataSourceV1: MealieDataSourceV1,
) : AddRecipeDataSource, RecipeDataSource, VersionDataSource { ) : AddRecipeDataSource, RecipeDataSource, VersionDataSource {
override suspend fun addRecipe(recipe: AddRecipeRequest): String = override suspend fun addRecipe(recipe: AddRecipeRequest): String =
withAuthHeader { token -> addRecipe(getUrl(), token, recipe) } withAuthHeader { token -> mealieDataSource.addRecipe(getUrl(), token, recipe) }
override suspend fun getVersionInfo(baseUrl: String): VersionInfo = override suspend fun getVersionInfo(baseUrl: String): VersionInfo =
mealieDataSource.getVersionInfo(baseUrl).toVersionInfo() mealieDataSource.getVersionInfo(baseUrl).toVersionInfo()
override suspend fun requestRecipes(start: Int, limit: Int): List<GetRecipeSummaryResponse> = override suspend fun requestRecipes(start: Int, limit: Int): List<GetRecipeSummaryResponseV1> =
withAuthHeader { token -> requestRecipes(getUrl(), token, start, limit) } withAuthHeader { token ->
runCatchingExceptCancel {
mealieDataSource.requestRecipes(getUrl(), token, start, limit).map {
GetRecipeSummaryResponseV1(
remoteId = it.remoteId.toString(),
name = it.name,
slug = it.slug,
image = it.image,
description = it.description,
recipeCategories = it.recipeCategories,
tags = it.tags,
rating = it.rating,
dateAdded = it.dateAdded,
dateUpdated = it.dateUpdated,
)
}
}.getOrElse {
if (it is NetworkError.NotMealie) {
mealieDataSourceV1.requestRecipes(getUrl(), token, start, limit)
} else {
throw it
}
}
}
override suspend fun requestRecipeInfo(slug: String): GetRecipeResponse = override suspend fun requestRecipeInfo(slug: String): GetRecipeResponse =
withAuthHeader { token -> requestRecipeInfo(getUrl(), token, slug) } withAuthHeader { token -> mealieDataSource.requestRecipeInfo(getUrl(), token, slug) }
private suspend fun getUrl() = baseURLStorage.requireBaseURL() private suspend fun getUrl() = baseURLStorage.requireBaseURL()
private suspend inline fun <T> withAuthHeader(block: MealieDataSource.(String?) -> T): T = private suspend inline fun <T> withAuthHeader(block: (String?) -> T): T =
mealieDataSource.runCatching { block(authRepo.getAuthHeader()) }.getOrElse { runCatching { block(authRepo.getAuthHeader()) }.getOrElse {
if (it is NetworkError.Unauthorized) { if (it is NetworkError.Unauthorized) {
authRepo.invalidateAuthHeader() authRepo.invalidateAuthHeader()
// Trying again with new authentication header // Trying again with new authentication header
mealieDataSource.block(authRepo.getAuthHeader()) block(authRepo.getAuthHeader())
} else { } else {
throw it throw it
} }

View File

@@ -4,14 +4,14 @@ import androidx.paging.PagingSource
import gq.kirmanak.mealient.database.recipe.entity.FullRecipeInfo import gq.kirmanak.mealient.database.recipe.entity.FullRecipeInfo
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
interface RecipeStorage { interface RecipeStorage {
suspend fun saveRecipes(recipes: List<GetRecipeSummaryResponse>) suspend fun saveRecipes(recipes: List<GetRecipeSummaryResponseV1>)
fun queryRecipes(): PagingSource<Int, RecipeSummaryEntity> fun queryRecipes(): PagingSource<Int, RecipeSummaryEntity>
suspend fun refreshAll(recipes: List<GetRecipeSummaryResponse>) suspend fun refreshAll(recipes: List<GetRecipeSummaryResponseV1>)
suspend fun clearAllLocalData() suspend fun clearAllLocalData()

View File

@@ -6,7 +6,7 @@ import gq.kirmanak.mealient.database.AppDb
import gq.kirmanak.mealient.database.recipe.RecipeDao import gq.kirmanak.mealient.database.recipe.RecipeDao
import gq.kirmanak.mealient.database.recipe.entity.* import gq.kirmanak.mealient.database.recipe.entity.*
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
import gq.kirmanak.mealient.extensions.recipeEntity import gq.kirmanak.mealient.extensions.recipeEntity
import gq.kirmanak.mealient.extensions.toRecipeEntity import gq.kirmanak.mealient.extensions.toRecipeEntity
import gq.kirmanak.mealient.extensions.toRecipeIngredientEntity import gq.kirmanak.mealient.extensions.toRecipeIngredientEntity
@@ -23,7 +23,7 @@ class RecipeStorageImpl @Inject constructor(
private val recipeDao: RecipeDao by lazy { db.recipeDao() } private val recipeDao: RecipeDao by lazy { db.recipeDao() }
override suspend fun saveRecipes( override suspend fun saveRecipes(
recipes: List<GetRecipeSummaryResponse> recipes: List<GetRecipeSummaryResponseV1>
) = db.withTransaction { ) = db.withTransaction {
logger.v { "saveRecipes() called with $recipes" } logger.v { "saveRecipes() called with $recipes" }
@@ -96,7 +96,7 @@ class RecipeStorageImpl @Inject constructor(
return recipeDao.queryRecipesByPages() return recipeDao.queryRecipesByPages()
} }
override suspend fun refreshAll(recipes: List<GetRecipeSummaryResponse>) { override suspend fun refreshAll(recipes: List<GetRecipeSummaryResponseV1>) {
logger.v { "refreshAll() called with: recipes = $recipes" } logger.v { "refreshAll() called with: recipes = $recipes" }
db.withTransaction { db.withTransaction {
recipeDao.removeAllRecipes() recipeDao.removeAllRecipes()

View File

@@ -1,10 +1,10 @@
package gq.kirmanak.mealient.data.recipes.network package gq.kirmanak.mealient.data.recipes.network
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
interface RecipeDataSource { interface RecipeDataSource {
suspend fun requestRecipes(start: Int, limit: Int): List<GetRecipeSummaryResponse> suspend fun requestRecipes(start: Int, limit: Int): List<GetRecipeSummaryResponseV1>
suspend fun requestRecipeInfo(slug: String): GetRecipeResponse suspend fun requestRecipeInfo(slug: String): GetRecipeResponse
} }

View File

@@ -6,6 +6,7 @@ import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.datasource.models.* import gq.kirmanak.mealient.datasource.models.*
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft
fun GetRecipeResponse.toRecipeEntity() = RecipeEntity( fun GetRecipeResponse.toRecipeEntity() = RecipeEntity(
@@ -31,7 +32,7 @@ fun GetRecipeInstructionResponse.toRecipeInstructionEntity(remoteId: String) =
text = text text = text
) )
fun GetRecipeSummaryResponse.recipeEntity() = RecipeSummaryEntity( fun GetRecipeSummaryResponseV1.recipeEntity() = RecipeSummaryEntity(
remoteId = remoteId, remoteId = remoteId,
name = name, name = name,
slug = slug, slug = slug,

View File

@@ -6,6 +6,9 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1Impl
import gq.kirmanak.mealient.datasource.v1.MealieServiceV1
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
@@ -52,6 +55,10 @@ interface DataSourceModule {
fun provideMealieService(retrofit: Retrofit): MealieService = fun provideMealieService(retrofit: Retrofit): MealieService =
retrofit.create() retrofit.create()
@Provides
@Singleton
fun provideMealieServiceV1(retrofit: Retrofit): MealieServiceV1 =
retrofit.create()
} }
@Binds @Binds
@@ -65,4 +72,8 @@ interface DataSourceModule {
@Binds @Binds
@Singleton @Singleton
fun bindMealieDataSource(mealientDataSourceImpl: MealieDataSourceImpl): MealieDataSource fun bindMealieDataSource(mealientDataSourceImpl: MealieDataSourceImpl): MealieDataSource
@Binds
@Singleton
fun bindMealieDataSourceV1(mealientDataSourceImpl: MealieDataSourceV1Impl): MealieDataSourceV1
} }

View File

@@ -73,20 +73,7 @@ class MealieDataSourceImpl @Inject constructor(
logParameters = { "baseUrl = $baseUrl, token = $token, start = $start, limit = $limit" } logParameters = { "baseUrl = $baseUrl, token = $token, start = $start, limit = $limit" }
).getOrElse { ).getOrElse {
val code = (it as? HttpException)?.code() ?: throw it val code = (it as? HttpException)?.code() ?: throw it
if (code == 404) requestRecipesV1(baseUrl, token, start, limit) else throw it if (code == 404) throw NetworkError.NotMealie(it) else throw it
}
private suspend fun requestRecipesV1(
baseUrl: String, token: String?, start: Int, limit: Int
): List<GetRecipeSummaryResponse> {
// Imagine start is 30 and limit is 15. It means that we already have page 1 and 2, now we need page 3
val perPage = limit
val page = start / perPage + 1
return makeCall(
block = { getRecipeSummaryV1("$baseUrl/api/recipes", token, page, perPage) },
logMethod = { "requestRecipesV1" },
logParameters = { "baseUrl = $baseUrl, token = $token, start = $start, limit = $limit" }
).map { it.items }.getOrThrowUnauthorized()
} }
override suspend fun requestRecipeInfo( override suspend fun requestRecipeInfo(

View File

@@ -1,41 +0,0 @@
package gq.kirmanak.mealient.datasource
import gq.kirmanak.mealient.datasource.models.AddRecipeRequest
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
import gq.kirmanak.mealient.datasource.models.VersionResponse
class MealieDataSourceV1Impl : MealieDataSourceV1 {
override suspend fun addRecipe(
baseUrl: String,
token: String?,
recipe: AddRecipeRequest
): String {
TODO("Not yet implemented")
}
override suspend fun authenticate(baseUrl: String, username: String, password: String): String {
TODO("Not yet implemented")
}
override suspend fun getVersionInfo(baseUrl: String): VersionResponse {
TODO("Not yet implemented")
}
override suspend fun requestRecipes(
baseUrl: String,
token: String?,
start: Int,
limit: Int
): List<GetRecipeSummaryResponse> {
TODO("Not yet implemented")
}
override suspend fun requestRecipeInfo(
baseUrl: String,
token: String?,
slug: String
): GetRecipeResponse {
TODO("Not yet implemented")
}
}

View File

@@ -34,14 +34,6 @@ interface MealieService {
@Query("limit") limit: Int, @Query("limit") limit: Int,
): List<GetRecipeSummaryResponse> ): List<GetRecipeSummaryResponse>
@GET
suspend fun getRecipeSummaryV1(
@Url url: String,
@Header(AUTHORIZATION_HEADER_NAME) token: String?,
@Query("page") page: Int,
@Query("perPage") perPage: Int,
): GetRecipesResponseV1
@GET @GET
suspend fun getRecipe( suspend fun getRecipe(
@Url url: String, @Url url: String,

View File

@@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
data class GetRecipeSummaryResponse( data class GetRecipeSummaryResponse(
@SerialName("id") val remoteId: String, @SerialName("id") val remoteId: Int,
@SerialName("name") val name: String, @SerialName("name") val name: String,
@SerialName("slug") val slug: String, @SerialName("slug") val slug: String,
@SerialName("image") val image: String?, @SerialName("image") val image: String?,

View File

@@ -1,9 +1,9 @@
package gq.kirmanak.mealient.datasource package gq.kirmanak.mealient.datasource.v1
import gq.kirmanak.mealient.datasource.models.AddRecipeRequest import gq.kirmanak.mealient.datasource.models.AddRecipeRequest
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
import gq.kirmanak.mealient.datasource.models.VersionResponse import gq.kirmanak.mealient.datasource.models.VersionResponse
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
interface MealieDataSourceV1 { interface MealieDataSourceV1 {
@@ -31,7 +31,7 @@ interface MealieDataSourceV1 {
token: String?, token: String?,
start: Int, start: Int,
limit: Int, limit: Int,
): List<GetRecipeSummaryResponse> ): List<GetRecipeSummaryResponseV1>
suspend fun requestRecipeInfo( suspend fun requestRecipeInfo(
baseUrl: String, baseUrl: String,

View File

@@ -0,0 +1,90 @@
package gq.kirmanak.mealient.datasource.v1
import gq.kirmanak.mealient.datasource.models.AddRecipeRequest
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
import gq.kirmanak.mealient.datasource.models.NetworkError
import gq.kirmanak.mealient.datasource.models.VersionResponse
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
import gq.kirmanak.mealient.logging.Logger
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import retrofit2.HttpException
import java.net.ConnectException
import java.net.SocketTimeoutException
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class MealieDataSourceV1Impl @Inject constructor(
private val logger: Logger,
private val mealieService: MealieServiceV1,
private val json: Json,
) : MealieDataSourceV1 {
override suspend fun addRecipe(
baseUrl: String,
token: String?,
recipe: AddRecipeRequest
): String {
TODO("Not yet implemented")
}
override suspend fun authenticate(baseUrl: String, username: String, password: String): String {
TODO("Not yet implemented")
}
override suspend fun getVersionInfo(baseUrl: String): VersionResponse = makeCall(
block = { getVersion("$baseUrl/api/app/about") },
logMethod = { "getVersionInfo" },
logParameters = { "baseUrl = $baseUrl" },
).getOrElse {
throw when (it) {
is HttpException, is SerializationException -> NetworkError.NotMealie(it)
is SocketTimeoutException, is ConnectException -> NetworkError.NoServerConnection(it)
else -> NetworkError.MalformedUrl(it)
}
}
override suspend fun requestRecipes(
baseUrl: String,
token: String?,
start: Int,
limit: Int
): List<GetRecipeSummaryResponseV1> {
// Imagine start is 30 and limit is 15. It means that we already have page 1 and 2, now we need page 3
val perPage = limit
val page = start / perPage + 1
return makeCall(
block = { getRecipeSummary("$baseUrl/api/recipes", token, page, perPage) },
logMethod = { "requestRecipesV1" },
logParameters = { "baseUrl = $baseUrl, token = $token, start = $start, limit = $limit" }
).map { it.items }.getOrThrowUnauthorized()
}
override suspend fun requestRecipeInfo(
baseUrl: String,
token: String?,
slug: String
): GetRecipeResponse {
TODO("Not yet implemented")
}
private suspend inline fun <T> makeCall(
crossinline block: suspend MealieServiceV1.() -> 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 <T> Result<T>.getOrThrowUnauthorized(): T = getOrElse {
throw if (it is HttpException && it.code() in listOf(401, 403)) {
NetworkError.Unauthorized(it)
} else {
it
}
}

View File

@@ -1,7 +1,8 @@
package gq.kirmanak.mealient.datasource package gq.kirmanak.mealient.datasource.v1
import gq.kirmanak.mealient.datasource.DataSourceModule.Companion.AUTHORIZATION_HEADER_NAME import gq.kirmanak.mealient.datasource.DataSourceModule.Companion.AUTHORIZATION_HEADER_NAME
import gq.kirmanak.mealient.datasource.models.* import gq.kirmanak.mealient.datasource.models.*
import gq.kirmanak.mealient.datasource.v1.models.GetRecipesResponseV1
import retrofit2.http.* import retrofit2.http.*
interface MealieServiceV1 { interface MealieServiceV1 {
@@ -28,14 +29,6 @@ interface MealieServiceV1 {
@GET @GET
suspend fun getRecipeSummary( suspend fun getRecipeSummary(
@Url url: String,
@Header(AUTHORIZATION_HEADER_NAME) token: String?,
@Query("start") start: Int,
@Query("limit") limit: Int,
): List<GetRecipeSummaryResponse>
@GET
suspend fun getRecipeSummaryV1(
@Url url: String, @Url url: String,
@Header(AUTHORIZATION_HEADER_NAME) token: String?, @Header(AUTHORIZATION_HEADER_NAME) token: String?,
@Query("page") page: Int, @Query("page") page: Int,

View File

@@ -0,0 +1,24 @@
package gq.kirmanak.mealient.datasource.v1.models
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetRecipeSummaryResponseV1(
@SerialName("id") val remoteId: String,
@SerialName("name") val name: String,
@SerialName("slug") val slug: String,
@SerialName("image") val image: String?,
@SerialName("description") val description: String = "",
@SerialName("recipeCategory") val recipeCategories: List<String>,
@SerialName("tags") val tags: List<String>,
@SerialName("rating") val rating: Int?,
@SerialName("dateAdded") val dateAdded: LocalDate,
@SerialName("dateUpdated") val dateUpdated: LocalDateTime
) {
override fun toString(): String {
return "GetRecipeSummaryResponseV1(remoteId=$remoteId, name='$name')"
}
}

View File

@@ -1,9 +1,9 @@
package gq.kirmanak.mealient.datasource.models package gq.kirmanak.mealient.datasource.v1.models
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@Serializable @Serializable
data class GetRecipesResponseV1( data class GetRecipesResponseV1(
@SerialName("items") val items: List<GetRecipeSummaryResponse>, @SerialName("items") val items: List<GetRecipeSummaryResponseV1>,
) )