Create network module
This commit is contained in:
@@ -8,7 +8,6 @@ plugins {
|
||||
id("kotlin-kapt")
|
||||
id("androidx.navigation.safeargs.kotlin")
|
||||
id("dagger.hilt.android.plugin")
|
||||
id("org.jetbrains.kotlin.plugin.serialization")
|
||||
id("com.google.gms.google-services")
|
||||
id("com.google.firebase.crashlytics")
|
||||
alias(libs.plugins.appsweep)
|
||||
@@ -19,8 +18,6 @@ android {
|
||||
applicationId = "gq.kirmanak.mealient"
|
||||
versionCode = 13
|
||||
versionName = "0.2.4"
|
||||
|
||||
buildConfigField("Boolean", "LOG_NETWORK", "false")
|
||||
}
|
||||
|
||||
signingConfigs {
|
||||
@@ -68,6 +65,7 @@ dependencies {
|
||||
|
||||
implementation(project(":database"))
|
||||
implementation(project(":datastore"))
|
||||
implementation(project(":datasource"))
|
||||
implementation(project(":logging"))
|
||||
|
||||
implementation(libs.android.material.material)
|
||||
@@ -92,16 +90,6 @@ dependencies {
|
||||
kaptTest(libs.google.dagger.hiltAndroidCompiler)
|
||||
testImplementation(libs.google.dagger.hiltAndroidTesting)
|
||||
|
||||
implementation(libs.squareup.retrofit)
|
||||
|
||||
implementation(libs.jakewharton.retrofitSerialization)
|
||||
|
||||
implementation(platform(libs.okhttp3.bom))
|
||||
implementation(libs.okhttp3.okhttp)
|
||||
debugImplementation(libs.okhttp3.loggingInterceptor)
|
||||
|
||||
implementation(libs.jetbrains.kotlinx.serialization)
|
||||
|
||||
implementation(libs.androidx.paging.runtimeKtx)
|
||||
testImplementation(libs.androidx.paging.commonKtx)
|
||||
|
||||
@@ -137,6 +125,4 @@ dependencies {
|
||||
testImplementation(libs.io.mockk)
|
||||
|
||||
debugImplementation(libs.squareup.leakcanary)
|
||||
|
||||
debugImplementation(libs.chuckerteam.chucker)
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
package gq.kirmanak.mealient.di
|
||||
|
||||
import android.content.Context
|
||||
import com.chuckerteam.chucker.api.ChuckerCollector
|
||||
import com.chuckerteam.chucker.api.ChuckerInterceptor
|
||||
import com.chuckerteam.chucker.api.RetentionManager
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import dagger.multibindings.IntoSet
|
||||
import gq.kirmanak.mealient.BuildConfig
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object DebugModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
@IntoSet
|
||||
fun provideLoggingInterceptor(logger: Logger): Interceptor {
|
||||
val interceptor = HttpLoggingInterceptor { message -> logger.v(tag = "OkHttp") { message } }
|
||||
interceptor.level = when {
|
||||
BuildConfig.LOG_NETWORK -> HttpLoggingInterceptor.Level.BODY
|
||||
else -> HttpLoggingInterceptor.Level.BASIC
|
||||
}
|
||||
return interceptor
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@IntoSet
|
||||
fun provideChuckerInterceptor(@ApplicationContext context: Context): Interceptor {
|
||||
val collector = ChuckerCollector(
|
||||
context = context,
|
||||
showNotification = true,
|
||||
retentionPeriod = RetentionManager.Period.ONE_HOUR,
|
||||
)
|
||||
return ChuckerInterceptor.Builder(context)
|
||||
.collector(collector)
|
||||
.alwaysReadResponseBody(true)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,7 @@
|
||||
package gq.kirmanak.mealient.data.add
|
||||
|
||||
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
||||
import gq.kirmanak.mealient.datasource.models.AddRecipeRequest
|
||||
|
||||
interface AddRecipeDataSource {
|
||||
|
||||
suspend fun addRecipe(recipe: AddRecipeRequest): String
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
package gq.kirmanak.mealient.data.add
|
||||
|
||||
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
||||
import gq.kirmanak.mealient.datasource.models.AddRecipeRequest
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface AddRecipeRepo {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
package gq.kirmanak.mealient.data.add.impl
|
||||
|
||||
import gq.kirmanak.mealient.data.add.AddRecipeDataSource
|
||||
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||
import gq.kirmanak.mealient.data.network.MealieDataSourceWrapper
|
||||
import gq.kirmanak.mealient.datasource.models.AddRecipeRequest
|
||||
import gq.kirmanak.mealient.extensions.logAndMapErrors
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import javax.inject.Inject
|
||||
@@ -10,15 +10,14 @@ import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AddRecipeDataSourceImpl @Inject constructor(
|
||||
private val addRecipeServiceFactory: ServiceFactory<AddRecipeService>,
|
||||
private val logger: Logger,
|
||||
private val mealieDataSourceWrapper: MealieDataSourceWrapper,
|
||||
) : AddRecipeDataSource {
|
||||
|
||||
override suspend fun addRecipe(recipe: AddRecipeRequest): String {
|
||||
logger.v { "addRecipe() called with: recipe = $recipe" }
|
||||
val service = addRecipeServiceFactory.provideService()
|
||||
val response = logger.logAndMapErrors(
|
||||
block = { service.addRecipe(recipe) },
|
||||
block = { mealieDataSourceWrapper.addRecipe(recipe) },
|
||||
logProvider = { "addRecipe: can't add recipe" }
|
||||
)
|
||||
logger.v { "addRecipe() response = $response" }
|
||||
|
||||
@@ -2,8 +2,10 @@ package gq.kirmanak.mealient.data.add.impl
|
||||
|
||||
import gq.kirmanak.mealient.data.add.AddRecipeDataSource
|
||||
import gq.kirmanak.mealient.data.add.AddRecipeRepo
|
||||
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
||||
import gq.kirmanak.mealient.datasource.models.AddRecipeRequest
|
||||
import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorage
|
||||
import gq.kirmanak.mealient.extensions.toAddRecipeRequest
|
||||
import gq.kirmanak.mealient.extensions.toDraft
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.first
|
||||
@@ -19,7 +21,7 @@ class AddRecipeRepoImpl @Inject constructor(
|
||||
) : AddRecipeRepo {
|
||||
|
||||
override val addRecipeRequestFlow: Flow<AddRecipeRequest>
|
||||
get() = addRecipeStorage.updates.map { AddRecipeRequest(it) }
|
||||
get() = addRecipeStorage.updates.map { it.toAddRecipeRequest() }
|
||||
|
||||
override suspend fun preserve(recipe: AddRecipeRequest) {
|
||||
logger.v { "preserveRecipe() called with: recipe = $recipe" }
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.add.impl
|
||||
|
||||
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
||||
import retrofit2.http.Body
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface AddRecipeService {
|
||||
|
||||
@POST("/api/recipes/create")
|
||||
suspend fun addRecipe(@Body addRecipeRequest: AddRecipeRequest): String
|
||||
|
||||
}
|
||||
@@ -1,77 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.add.models
|
||||
|
||||
import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AddRecipeRequest(
|
||||
@SerialName("name") val name: String = "",
|
||||
@SerialName("description") val description: String = "",
|
||||
@SerialName("image") val image: String = "",
|
||||
@SerialName("recipeYield") val recipeYield: String = "",
|
||||
@SerialName("recipeIngredient") val recipeIngredient: List<AddRecipeIngredient> = emptyList(),
|
||||
@SerialName("recipeInstructions") val recipeInstructions: List<AddRecipeInstruction> = emptyList(),
|
||||
@SerialName("slug") val slug: String = "",
|
||||
@SerialName("filePath") val filePath: String = "",
|
||||
@SerialName("tags") val tags: List<String> = emptyList(),
|
||||
@SerialName("categories") val categories: List<String> = emptyList(),
|
||||
@SerialName("notes") val notes: List<AddRecipeNote> = emptyList(),
|
||||
@SerialName("extras") val extras: Map<String, String> = emptyMap(),
|
||||
@SerialName("assets") val assets: List<String> = emptyList(),
|
||||
@SerialName("settings") val settings: AddRecipeSettings = AddRecipeSettings(),
|
||||
) {
|
||||
constructor(input: AddRecipeDraft) : this(
|
||||
name = input.recipeName,
|
||||
description = input.recipeDescription,
|
||||
recipeYield = input.recipeYield,
|
||||
recipeIngredient = input.recipeIngredients.map { AddRecipeIngredient(note = it) },
|
||||
recipeInstructions = input.recipeInstructions.map { AddRecipeInstruction(text = it) },
|
||||
settings = AddRecipeSettings(
|
||||
public = input.isRecipePublic,
|
||||
disableComments = input.areCommentsDisabled,
|
||||
)
|
||||
)
|
||||
|
||||
fun toDraft(): AddRecipeDraft = AddRecipeDraft(
|
||||
recipeName = name,
|
||||
recipeDescription = description,
|
||||
recipeYield = recipeYield,
|
||||
recipeInstructions = recipeInstructions.map { it.text },
|
||||
recipeIngredients = recipeIngredient.map { it.note },
|
||||
isRecipePublic = settings.public,
|
||||
areCommentsDisabled = settings.disableComments,
|
||||
)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class AddRecipeSettings(
|
||||
@SerialName("disableAmount") val disableAmount: Boolean = true,
|
||||
@SerialName("disableComments") val disableComments: Boolean = false,
|
||||
@SerialName("landscapeView") val landscapeView: Boolean = true,
|
||||
@SerialName("public") val public: Boolean = true,
|
||||
@SerialName("showAssets") val showAssets: Boolean = true,
|
||||
@SerialName("showNutrition") val showNutrition: Boolean = true,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AddRecipeNote(
|
||||
@SerialName("title") val title: String = "",
|
||||
@SerialName("text") val text: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AddRecipeInstruction(
|
||||
@SerialName("title") val title: String = "",
|
||||
@SerialName("text") val text: String = "",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AddRecipeIngredient(
|
||||
@SerialName("disableAmount") val disableAmount: Boolean = true,
|
||||
@SerialName("food") val food: String? = null,
|
||||
@SerialName("note") val note: String = "",
|
||||
@SerialName("quantity") val quantity: Int = 1,
|
||||
@SerialName("title") val title: String? = null,
|
||||
@SerialName("unit") val unit: String? = null,
|
||||
)
|
||||
@@ -4,5 +4,5 @@ interface AuthDataSource {
|
||||
/**
|
||||
* Tries to acquire authentication token using the provided credentials
|
||||
*/
|
||||
suspend fun authenticate(username: String, password: String): String
|
||||
suspend fun authenticate(username: String, password: String, baseUrl: String): String
|
||||
}
|
||||
@@ -1,56 +1,25 @@
|
||||
package gq.kirmanak.mealient.data.auth.impl
|
||||
|
||||
import gq.kirmanak.mealient.data.auth.AuthDataSource
|
||||
import gq.kirmanak.mealient.data.network.ErrorDetail
|
||||
import gq.kirmanak.mealient.data.network.NetworkError.NotMealie
|
||||
import gq.kirmanak.mealient.data.network.NetworkError.Unauthorized
|
||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||
import gq.kirmanak.mealient.extensions.decodeErrorBody
|
||||
import gq.kirmanak.mealient.datasource.MealieDataSource
|
||||
import gq.kirmanak.mealient.extensions.logAndMapErrors
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import kotlinx.serialization.json.Json
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AuthDataSourceImpl @Inject constructor(
|
||||
private val authServiceFactory: ServiceFactory<AuthService>,
|
||||
private val json: Json,
|
||||
private val logger: Logger,
|
||||
private val mealieDataSource: MealieDataSource,
|
||||
) : AuthDataSource {
|
||||
|
||||
override suspend fun authenticate(username: String, password: String): String {
|
||||
override suspend fun authenticate(username: String, password: String, baseUrl: String): String {
|
||||
logger.v { "authenticate() called with: username = $username, password = $password" }
|
||||
val authService = authServiceFactory.provideService()
|
||||
val response = sendRequest(authService, username, password)
|
||||
val accessToken = parseToken(response)
|
||||
val accessToken = logger.logAndMapErrors(
|
||||
block = { mealieDataSource.authenticate(baseUrl, username, password) },
|
||||
logProvider = { "sendRequest: can't get token" },
|
||||
)
|
||||
logger.v { "authenticate() returned: $accessToken" }
|
||||
return accessToken
|
||||
}
|
||||
|
||||
private suspend fun sendRequest(
|
||||
authService: AuthService,
|
||||
username: String,
|
||||
password: String
|
||||
): Response<GetTokenResponse> = logger.logAndMapErrors(
|
||||
block = { authService.getToken(username = username, password = password) },
|
||||
logProvider = { "sendRequest: can't get token" },
|
||||
)
|
||||
|
||||
private fun parseToken(
|
||||
response: Response<GetTokenResponse>
|
||||
): String = if (response.isSuccessful) {
|
||||
response.body()?.accessToken ?: throw 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" -> Unauthorized(cause)
|
||||
else -> NotMealie(cause)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package gq.kirmanak.mealient.data.auth.impl
|
||||
import gq.kirmanak.mealient.data.auth.AuthDataSource
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
||||
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
@@ -14,6 +15,7 @@ import javax.inject.Singleton
|
||||
class AuthRepoImpl @Inject constructor(
|
||||
private val authStorage: AuthStorage,
|
||||
private val authDataSource: AuthDataSource,
|
||||
private val baseURLStorage: BaseURLStorage,
|
||||
private val logger: Logger,
|
||||
) : AuthRepo {
|
||||
|
||||
@@ -22,7 +24,7 @@ class AuthRepoImpl @Inject constructor(
|
||||
|
||||
override suspend fun authenticate(email: String, password: String) {
|
||||
logger.v { "authenticate() called with: email = $email, password = $password" }
|
||||
authDataSource.authenticate(email, password)
|
||||
authDataSource.authenticate(email, password, baseURLStorage.requireBaseURL())
|
||||
.let { AUTH_HEADER_FORMAT.format(it) }
|
||||
.let { authStorage.setAuthHeader(it) }
|
||||
authStorage.setEmail(email)
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.auth.impl
|
||||
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Field
|
||||
import retrofit2.http.FormUrlEncoded
|
||||
import retrofit2.http.POST
|
||||
|
||||
interface AuthService {
|
||||
@FormUrlEncoded
|
||||
@POST("/api/auth/token")
|
||||
suspend fun getToken(
|
||||
@Field("username") username: String,
|
||||
@Field("password") password: String,
|
||||
): Response<GetTokenResponse>
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.auth.impl
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetTokenResponse(@SerialName("access_token") val accessToken: String)
|
||||
@@ -1,9 +1,6 @@
|
||||
package gq.kirmanak.mealient.data.baseurl
|
||||
|
||||
import gq.kirmanak.mealient.data.network.NetworkError
|
||||
|
||||
interface VersionDataSource {
|
||||
|
||||
@Throws(NetworkError::class)
|
||||
suspend fun getVersionInfo(baseUrl: String): VersionInfo
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package gq.kirmanak.mealient.data.baseurl.impl
|
||||
|
||||
import gq.kirmanak.mealient.data.baseurl.VersionDataSource
|
||||
import gq.kirmanak.mealient.data.baseurl.VersionInfo
|
||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||
import gq.kirmanak.mealient.data.network.MealieDataSourceWrapper
|
||||
import gq.kirmanak.mealient.extensions.logAndMapErrors
|
||||
import gq.kirmanak.mealient.extensions.versionInfo
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
@@ -11,16 +11,15 @@ import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class VersionDataSourceImpl @Inject constructor(
|
||||
private val serviceFactory: ServiceFactory<VersionService>,
|
||||
private val logger: Logger,
|
||||
private val mealieDataSourceWrapper: MealieDataSourceWrapper,
|
||||
) : VersionDataSource {
|
||||
|
||||
override suspend fun getVersionInfo(baseUrl: String): VersionInfo {
|
||||
logger.v { "getVersionInfo() called with: baseUrl = $baseUrl" }
|
||||
|
||||
val service = serviceFactory.provideService(baseUrl)
|
||||
val response = logger.logAndMapErrors(
|
||||
block = { service.getVersion() },
|
||||
block = { mealieDataSourceWrapper.getVersionInfo(baseUrl) },
|
||||
logProvider = { "getVersionInfo: can't request version" }
|
||||
)
|
||||
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.baseurl.impl
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class VersionResponse(
|
||||
@SerialName("production")
|
||||
val production: Boolean,
|
||||
@SerialName("version")
|
||||
val version: String,
|
||||
@SerialName("demoStatus")
|
||||
val demoStatus: Boolean,
|
||||
)
|
||||
@@ -1,8 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.baseurl.impl
|
||||
|
||||
import retrofit2.http.GET
|
||||
|
||||
interface VersionService {
|
||||
@GET("api/debug/version")
|
||||
suspend fun getVersion(): VersionResponse
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.network
|
||||
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.Response
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class AuthenticationInterceptor @Inject constructor(
|
||||
private val authRepo: AuthRepo,
|
||||
) : Interceptor {
|
||||
|
||||
private val authHeader: String?
|
||||
get() = runBlocking { authRepo.getAuthHeader() }
|
||||
|
||||
override fun intercept(chain: Interceptor.Chain): Response {
|
||||
val currentHeader = authHeader ?: return chain.proceed(chain.request())
|
||||
val response = proceedWithAuthHeader(chain, currentHeader)
|
||||
return if (listOf(401, 403).contains(response.code)) {
|
||||
runBlocking { authRepo.invalidateAuthHeader() }
|
||||
// Try again with new auth header (if any) or return previous response
|
||||
authHeader?.let { proceedWithAuthHeader(chain, it) } ?: response
|
||||
} else {
|
||||
response
|
||||
}
|
||||
}
|
||||
|
||||
private fun proceedWithAuthHeader(
|
||||
chain: Interceptor.Chain,
|
||||
authHeader: String,
|
||||
) = chain.proceed(
|
||||
chain.request()
|
||||
.newBuilder()
|
||||
.header(HEADER_NAME, authHeader)
|
||||
.build()
|
||||
)
|
||||
|
||||
companion object {
|
||||
private const val HEADER_NAME = "Authorization"
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.network
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ErrorDetail(@SerialName("detail") val detail: String? = null)
|
||||
@@ -0,0 +1,49 @@
|
||||
package gq.kirmanak.mealient.data.network
|
||||
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
||||
import gq.kirmanak.mealient.datasource.MealieDataSource
|
||||
import gq.kirmanak.mealient.datasource.models.*
|
||||
import gq.kirmanak.mealient.datasource.models.NetworkError
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class MealieDataSourceWrapper @Inject constructor(
|
||||
private val baseURLStorage: BaseURLStorage,
|
||||
private val authRepo: AuthRepo,
|
||||
private val mealieDataSource: MealieDataSource,
|
||||
) {
|
||||
|
||||
suspend fun addRecipe(recipe: AddRecipeRequest): String {
|
||||
val baseUrl = baseURLStorage.requireBaseURL()
|
||||
return withAuthHeader { token -> addRecipe(baseUrl, token, recipe) }
|
||||
}
|
||||
|
||||
suspend fun getVersionInfo(baseUrl: String): VersionResponse {
|
||||
return mealieDataSource.getVersionInfo(baseUrl)
|
||||
}
|
||||
|
||||
suspend fun requestRecipes(
|
||||
start: Int = 0,
|
||||
limit: Int = 9999,
|
||||
): List<GetRecipeSummaryResponse> {
|
||||
val baseUrl = baseURLStorage.requireBaseURL()
|
||||
return withAuthHeader { token -> requestRecipes(baseUrl, token, start, limit) }
|
||||
}
|
||||
|
||||
suspend fun requestRecipeInfo(slug: String): GetRecipeResponse {
|
||||
val baseUrl = baseURLStorage.requireBaseURL()
|
||||
return withAuthHeader { token -> requestRecipeInfo(baseUrl, token, slug) }
|
||||
}
|
||||
|
||||
private suspend inline fun <T> withAuthHeader(block: MealieDataSource.(String?) -> T): T =
|
||||
mealieDataSource.runCatching { block(authRepo.getAuthHeader()) }.getOrElse {
|
||||
if (it is NetworkError.Unauthorized) {
|
||||
authRepo.invalidateAuthHeader()
|
||||
mealieDataSource.block(authRepo.getAuthHeader())
|
||||
} else {
|
||||
throw it
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.network
|
||||
|
||||
sealed class NetworkError(cause: Throwable) : RuntimeException(cause) {
|
||||
class Unauthorized(cause: Throwable) : NetworkError(cause)
|
||||
class NoServerConnection(cause: Throwable) : NetworkError(cause)
|
||||
class NotMealie(cause: Throwable) : NetworkError(cause)
|
||||
class MalformedUrl(cause: Throwable) : NetworkError(cause)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.network
|
||||
|
||||
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.OkHttpClient
|
||||
import retrofit2.Retrofit
|
||||
|
||||
class RetrofitBuilder(
|
||||
private val okHttpClient: OkHttpClient,
|
||||
private val json: Json,
|
||||
private val logger: Logger,
|
||||
) {
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
fun buildRetrofit(baseUrl: String): Retrofit {
|
||||
logger.v { "buildRetrofit() called with: baseUrl = $baseUrl" }
|
||||
val contentType = "application/json".toMediaType()
|
||||
val converterFactory = json.asConverterFactory(contentType)
|
||||
return Retrofit.Builder()
|
||||
.baseUrl(baseUrl)
|
||||
.client(okHttpClient)
|
||||
.addConverterFactory(converterFactory)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.network
|
||||
|
||||
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
||||
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
|
||||
inline fun <reified T> RetrofitBuilder.createServiceFactory(
|
||||
baseURLStorage: BaseURLStorage,
|
||||
logger: Logger
|
||||
) =
|
||||
RetrofitServiceFactory(T::class.java, this, baseURLStorage, logger)
|
||||
|
||||
class RetrofitServiceFactory<T>(
|
||||
private val serviceClass: Class<T>,
|
||||
private val retrofitBuilder: RetrofitBuilder,
|
||||
private val baseURLStorage: BaseURLStorage,
|
||||
private val logger: Logger,
|
||||
) : ServiceFactory<T> {
|
||||
|
||||
private val cache: MutableMap<String, T> = mutableMapOf()
|
||||
|
||||
override suspend fun provideService(baseUrl: String?): T = runCatchingExceptCancel {
|
||||
logger.v { "provideService() called with: baseUrl = $baseUrl, class = ${serviceClass.simpleName}" }
|
||||
val url = baseUrl ?: baseURLStorage.requireBaseURL()
|
||||
synchronized(cache) { cache[url] ?: createService(url, serviceClass) }
|
||||
}.getOrElse {
|
||||
logger.e(it) { "provideService: can't provide service for $baseUrl" }
|
||||
throw NetworkError.MalformedUrl(it)
|
||||
}
|
||||
|
||||
private fun createService(url: String, serviceClass: Class<T>): T {
|
||||
logger.v { "createService() called with: url = $url, serviceClass = ${serviceClass.simpleName}" }
|
||||
val service = retrofitBuilder.buildRetrofit(url).create(serviceClass)
|
||||
cache[url] = service
|
||||
return service
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.network
|
||||
|
||||
interface ServiceFactory<T> {
|
||||
|
||||
suspend fun provideService(baseUrl: String? = null): T
|
||||
}
|
||||
@@ -1,10 +1,10 @@
|
||||
package gq.kirmanak.mealient.data.recipes.db
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
|
||||
import gq.kirmanak.mealient.database.recipe.entity.FullRecipeInfo
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
|
||||
|
||||
interface RecipeStorage {
|
||||
suspend fun saveRecipes(recipes: List<GetRecipeSummaryResponse>)
|
||||
|
||||
@@ -2,11 +2,11 @@ package gq.kirmanak.mealient.data.recipes.db
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import androidx.room.withTransaction
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
|
||||
import gq.kirmanak.mealient.database.AppDb
|
||||
import gq.kirmanak.mealient.database.recipe.RecipeDao
|
||||
import gq.kirmanak.mealient.database.recipe.entity.*
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
|
||||
import gq.kirmanak.mealient.extensions.recipeEntity
|
||||
import gq.kirmanak.mealient.extensions.toRecipeEntity
|
||||
import gq.kirmanak.mealient.extensions.toRecipeIngredientEntity
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package gq.kirmanak.mealient.data.recipes.network
|
||||
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
|
||||
|
||||
interface RecipeDataSource {
|
||||
suspend fun requestRecipes(start: Int = 0, limit: Int = 9999): List<GetRecipeSummaryResponse>
|
||||
|
||||
@@ -1,34 +1,30 @@
|
||||
package gq.kirmanak.mealient.data.recipes.network
|
||||
|
||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
|
||||
import gq.kirmanak.mealient.data.network.MealieDataSourceWrapper
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class RecipeDataSourceImpl @Inject constructor(
|
||||
private val recipeServiceFactory: ServiceFactory<RecipeService>,
|
||||
private val logger: Logger,
|
||||
private val mealieDataSourceWrapper: MealieDataSourceWrapper,
|
||||
) : RecipeDataSource {
|
||||
|
||||
override suspend fun requestRecipes(start: Int, limit: Int): List<GetRecipeSummaryResponse> {
|
||||
logger.v { "requestRecipes() called with: start = $start, limit = $limit" }
|
||||
val recipeSummary = getRecipeService().getRecipeSummary(start, limit)
|
||||
val recipeSummary = mealieDataSourceWrapper.requestRecipes(start, limit)
|
||||
logger.v { "requestRecipes() returned: $recipeSummary" }
|
||||
return recipeSummary
|
||||
}
|
||||
|
||||
override suspend fun requestRecipeInfo(slug: String): GetRecipeResponse {
|
||||
logger.v { "requestRecipeInfo() called with: slug = $slug" }
|
||||
val recipeInfo = getRecipeService().getRecipe(slug)
|
||||
val recipeInfo = mealieDataSourceWrapper.requestRecipeInfo(slug)
|
||||
logger.v { "requestRecipeInfo() returned: $recipeInfo" }
|
||||
return recipeInfo
|
||||
}
|
||||
|
||||
private suspend fun getRecipeService(): RecipeService {
|
||||
logger.v { "getRecipeService() called" }
|
||||
return recipeServiceFactory.provideService()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.recipes.network
|
||||
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
|
||||
import retrofit2.http.GET
|
||||
import retrofit2.http.Path
|
||||
import retrofit2.http.Query
|
||||
|
||||
interface RecipeService {
|
||||
@GET("/api/recipes/summary")
|
||||
suspend fun getRecipeSummary(
|
||||
@Query("start") start: Int,
|
||||
@Query("limit") limit: Int,
|
||||
): List<GetRecipeSummaryResponse>
|
||||
|
||||
@GET("/api/recipes/{recipe_slug}")
|
||||
suspend fun getRecipe(
|
||||
@Path("recipe_slug") recipeSlug: String,
|
||||
): GetRecipeResponse
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.recipes.network.response
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetRecipeIngredientResponse(
|
||||
@SerialName("title") val title: String = "",
|
||||
@SerialName("note") val note: String = "",
|
||||
@SerialName("unit") val unit: String = "",
|
||||
@SerialName("food") val food: String = "",
|
||||
@SerialName("disableAmount") val disableAmount: Boolean,
|
||||
@SerialName("quantity") val quantity: Int,
|
||||
)
|
||||
@@ -1,10 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.recipes.network.response
|
||||
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetRecipeInstructionResponse(
|
||||
@SerialName("title") val title: String = "",
|
||||
@SerialName("text") val text: String,
|
||||
)
|
||||
@@ -1,23 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.recipes.network.response
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetRecipeResponse(
|
||||
@SerialName("id") val remoteId: Long,
|
||||
@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,
|
||||
@SerialName("recipeYield") val recipeYield: String = "",
|
||||
@SerialName("recipeIngredient") val recipeIngredients: List<GetRecipeIngredientResponse>,
|
||||
@SerialName("recipeInstructions") val recipeInstructions: List<GetRecipeInstructionResponse>,
|
||||
)
|
||||
@@ -1,24 +0,0 @@
|
||||
package gq.kirmanak.mealient.data.recipes.network.response
|
||||
|
||||
import kotlinx.datetime.LocalDate
|
||||
import kotlinx.datetime.LocalDateTime
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class GetRecipeSummaryResponse(
|
||||
@SerialName("id") val remoteId: Long,
|
||||
@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 "GetRecipeSummaryResponse(remoteId=$remoteId, name='$name')"
|
||||
}
|
||||
}
|
||||
@@ -2,46 +2,20 @@ package gq.kirmanak.mealient.di
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import gq.kirmanak.mealient.data.add.AddRecipeDataSource
|
||||
import gq.kirmanak.mealient.data.add.AddRecipeRepo
|
||||
import gq.kirmanak.mealient.data.add.impl.AddRecipeDataSourceImpl
|
||||
import gq.kirmanak.mealient.data.add.impl.AddRecipeRepoImpl
|
||||
import gq.kirmanak.mealient.data.add.impl.AddRecipeService
|
||||
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
||||
import gq.kirmanak.mealient.data.network.RetrofitBuilder
|
||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||
import gq.kirmanak.mealient.data.network.createServiceFactory
|
||||
import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorage
|
||||
import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorageImpl
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface AddRecipeModule {
|
||||
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAddRecipeServiceFactory(
|
||||
@Named(AUTH_OK_HTTP) okHttpClient: OkHttpClient,
|
||||
json: Json,
|
||||
logger: Logger,
|
||||
baseURLStorage: BaseURLStorage,
|
||||
): ServiceFactory<AddRecipeService> {
|
||||
return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory(
|
||||
baseURLStorage,
|
||||
logger
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
|
||||
@@ -13,16 +13,7 @@ import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.auth.AuthStorage
|
||||
import gq.kirmanak.mealient.data.auth.impl.AuthDataSourceImpl
|
||||
import gq.kirmanak.mealient.data.auth.impl.AuthRepoImpl
|
||||
import gq.kirmanak.mealient.data.auth.impl.AuthService
|
||||
import gq.kirmanak.mealient.data.auth.impl.AuthStorageImpl
|
||||
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
||||
import gq.kirmanak.mealient.data.network.RetrofitBuilder
|
||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||
import gq.kirmanak.mealient.data.network.createServiceFactory
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@@ -31,20 +22,6 @@ interface AuthModule {
|
||||
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAuthServiceFactory(
|
||||
@Named(NO_AUTH_OK_HTTP) okHttpClient: OkHttpClient,
|
||||
json: Json,
|
||||
logger: Logger,
|
||||
baseURLStorage: BaseURLStorage,
|
||||
): ServiceFactory<AuthService> {
|
||||
return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory(
|
||||
baseURLStorage,
|
||||
logger
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideAccountManager(@ApplicationContext context: Context): AccountManager {
|
||||
|
||||
@@ -2,44 +2,18 @@ package gq.kirmanak.mealient.di
|
||||
|
||||
import dagger.Binds
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
||||
import gq.kirmanak.mealient.data.baseurl.VersionDataSource
|
||||
import gq.kirmanak.mealient.data.baseurl.impl.BaseURLStorageImpl
|
||||
import gq.kirmanak.mealient.data.baseurl.impl.VersionDataSourceImpl
|
||||
import gq.kirmanak.mealient.data.baseurl.impl.VersionService
|
||||
import gq.kirmanak.mealient.data.network.RetrofitBuilder
|
||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||
import gq.kirmanak.mealient.data.network.createServiceFactory
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
interface BaseURLModule {
|
||||
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideVersionServiceFactory(
|
||||
@Named(AUTH_OK_HTTP) okHttpClient: OkHttpClient,
|
||||
json: Json,
|
||||
logger: Logger,
|
||||
baseURLStorage: BaseURLStorage,
|
||||
): ServiceFactory<VersionService> {
|
||||
return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory(
|
||||
baseURLStorage,
|
||||
logger
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Binds
|
||||
@Singleton
|
||||
fun bindVersionDataSource(versionDataSourceImpl: VersionDataSourceImpl): VersionDataSource
|
||||
|
||||
@@ -8,7 +8,6 @@ import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.InputStream
|
||||
import javax.inject.Named
|
||||
|
||||
@EntryPoint
|
||||
@InstallIn(SingletonComponent::class)
|
||||
@@ -16,7 +15,6 @@ interface GlideModuleEntryPoint {
|
||||
|
||||
fun provideLogger(): Logger
|
||||
|
||||
@Named(AUTH_OK_HTTP)
|
||||
fun provideOkHttp(): OkHttpClient
|
||||
|
||||
fun provideRecipeLoaderFactory(): ModelLoaderFactory<RecipeSummaryEntity, InputStream>
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
package gq.kirmanak.mealient.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import gq.kirmanak.mealient.data.network.AuthenticationInterceptor
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
const val AUTH_OK_HTTP = "auth"
|
||||
const val NO_AUTH_OK_HTTP = "noauth"
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object NetworkModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named(AUTH_OK_HTTP)
|
||||
fun createAuthOkHttp(
|
||||
// Use @JvmSuppressWildcards because otherwise dagger can't inject it (https://stackoverflow.com/a/43149382)
|
||||
interceptors: Set<@JvmSuppressWildcards Interceptor>,
|
||||
authenticationInterceptor: AuthenticationInterceptor,
|
||||
): OkHttpClient = OkHttpClient.Builder()
|
||||
.addInterceptor(authenticationInterceptor)
|
||||
.apply { for (interceptor in interceptors) addNetworkInterceptor(interceptor) }
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
@Named(NO_AUTH_OK_HTTP)
|
||||
fun createNoAuthOkHttp(
|
||||
// Use @JvmSuppressWildcards because otherwise dagger can't inject it (https://stackoverflow.com/a/43149382)
|
||||
interceptors: Set<@JvmSuppressWildcards Interceptor>,
|
||||
): OkHttpClient = OkHttpClient.Builder()
|
||||
.apply { for (interceptor in interceptors) addNetworkInterceptor(interceptor) }
|
||||
.build()
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun createJson(): Json = Json {
|
||||
coerceInputValues = true
|
||||
ignoreUnknownKeys = true
|
||||
encodeDefaults = true
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,6 @@ import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage
|
||||
import gq.kirmanak.mealient.data.network.RetrofitBuilder
|
||||
import gq.kirmanak.mealient.data.network.ServiceFactory
|
||||
import gq.kirmanak.mealient.data.network.createServiceFactory
|
||||
import gq.kirmanak.mealient.data.recipes.RecipeRepo
|
||||
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
|
||||
import gq.kirmanak.mealient.data.recipes.db.RecipeStorageImpl
|
||||
@@ -21,14 +17,9 @@ import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProviderImpl
|
||||
import gq.kirmanak.mealient.data.recipes.impl.RecipeRepoImpl
|
||||
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
|
||||
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSourceImpl
|
||||
import gq.kirmanak.mealient.data.recipes.network.RecipeService
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import gq.kirmanak.mealient.ui.recipes.images.RecipeModelLoaderFactory
|
||||
import kotlinx.serialization.json.Json
|
||||
import okhttp3.OkHttpClient
|
||||
import java.io.InputStream
|
||||
import javax.inject.Named
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@@ -57,20 +48,6 @@ interface RecipeModule {
|
||||
|
||||
companion object {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRecipeServiceFactory(
|
||||
@Named(AUTH_OK_HTTP) okHttpClient: OkHttpClient,
|
||||
json: Json,
|
||||
logger: Logger,
|
||||
baseURLStorage: BaseURLStorage,
|
||||
): ServiceFactory<RecipeService> {
|
||||
return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory(
|
||||
baseURLStorage,
|
||||
logger
|
||||
)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideRecipePagingSourceFactory(
|
||||
|
||||
@@ -1,25 +1,7 @@
|
||||
package gq.kirmanak.mealient.extensions
|
||||
|
||||
import gq.kirmanak.mealient.data.network.NetworkError
|
||||
import gq.kirmanak.mealient.datasource.mapToNetworkError
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.SerializationException
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import retrofit2.HttpException
|
||||
import retrofit2.Response
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
inline fun <T, reified R> Json.decodeErrorBody(response: Response<T>): R =
|
||||
checkNotNull(response.errorBody()) { "Can't decode absent error body" }
|
||||
.byteStream()
|
||||
.let(::decodeFromStream)
|
||||
|
||||
|
||||
fun Throwable.mapToNetworkError(): NetworkError = when (this) {
|
||||
is HttpException, is SerializationException -> NetworkError.NotMealie(this)
|
||||
else -> NetworkError.NoServerConnection(this)
|
||||
}
|
||||
|
||||
inline fun <T> Logger.logAndMapErrors(
|
||||
block: () -> T,
|
||||
|
||||
@@ -1,15 +1,12 @@
|
||||
package gq.kirmanak.mealient.extensions
|
||||
|
||||
import gq.kirmanak.mealient.data.baseurl.VersionInfo
|
||||
import gq.kirmanak.mealient.data.baseurl.impl.VersionResponse
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeIngredientResponse
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeInstructionResponse
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeResponse
|
||||
import gq.kirmanak.mealient.data.recipes.network.response.GetRecipeSummaryResponse
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeIngredientEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeInstructionEntity
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.datasource.models.*
|
||||
import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft
|
||||
|
||||
fun GetRecipeResponse.toRecipeEntity() = RecipeEntity(
|
||||
remoteId = remoteId,
|
||||
@@ -45,4 +42,26 @@ fun GetRecipeSummaryResponse.recipeEntity() = RecipeSummaryEntity(
|
||||
dateUpdated = dateUpdated,
|
||||
)
|
||||
|
||||
fun VersionResponse.versionInfo() = VersionInfo(production, version, demoStatus)
|
||||
fun VersionResponse.versionInfo() = VersionInfo(production, version, demoStatus)
|
||||
|
||||
fun AddRecipeDraft.toAddRecipeRequest() = AddRecipeRequest(
|
||||
name = recipeName,
|
||||
description = recipeDescription,
|
||||
recipeYield = recipeYield,
|
||||
recipeIngredient = recipeIngredients.map { AddRecipeIngredient(note = it) },
|
||||
recipeInstructions = recipeInstructions.map { AddRecipeInstruction(text = it) },
|
||||
settings = AddRecipeSettings(
|
||||
public = isRecipePublic,
|
||||
disableComments = areCommentsDisabled,
|
||||
)
|
||||
)
|
||||
|
||||
fun AddRecipeRequest.toDraft(): AddRecipeDraft = AddRecipeDraft(
|
||||
recipeName = name,
|
||||
recipeDescription = description,
|
||||
recipeYield = recipeYield,
|
||||
recipeInstructions = recipeInstructions.map { it.text },
|
||||
recipeIngredients = recipeIngredient.map { it.note },
|
||||
isRecipePublic = settings.public,
|
||||
areCommentsDisabled = settings.disableComments,
|
||||
)
|
||||
|
||||
@@ -12,12 +12,12 @@ import androidx.fragment.app.viewModels
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.data.add.models.AddRecipeIngredient
|
||||
import gq.kirmanak.mealient.data.add.models.AddRecipeInstruction
|
||||
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
||||
import gq.kirmanak.mealient.data.add.models.AddRecipeSettings
|
||||
import gq.kirmanak.mealient.databinding.FragmentAddRecipeBinding
|
||||
import gq.kirmanak.mealient.databinding.ViewSingleInputBinding
|
||||
import gq.kirmanak.mealient.datasource.models.AddRecipeIngredient
|
||||
import gq.kirmanak.mealient.datasource.models.AddRecipeInstruction
|
||||
import gq.kirmanak.mealient.datasource.models.AddRecipeRequest
|
||||
import gq.kirmanak.mealient.datasource.models.AddRecipeSettings
|
||||
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
|
||||
import gq.kirmanak.mealient.extensions.collectWhenViewResumed
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
|
||||
@@ -4,7 +4,7 @@ import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import gq.kirmanak.mealient.data.add.AddRecipeRepo
|
||||
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
|
||||
import gq.kirmanak.mealient.datasource.models.AddRecipeRequest
|
||||
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import kotlinx.coroutines.channels.Channel
|
||||
|
||||
@@ -9,8 +9,8 @@ import androidx.navigation.fragment.findNavController
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.data.network.NetworkError
|
||||
import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding
|
||||
import gq.kirmanak.mealient.datasource.models.NetworkError
|
||||
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import gq.kirmanak.mealient.ui.OperationUiState
|
||||
|
||||
@@ -9,8 +9,8 @@ import androidx.navigation.fragment.findNavController
|
||||
import by.kirich1409.viewbindingdelegate.viewBinding
|
||||
import dagger.hilt.android.AndroidEntryPoint
|
||||
import gq.kirmanak.mealient.R
|
||||
import gq.kirmanak.mealient.data.network.NetworkError
|
||||
import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding
|
||||
import gq.kirmanak.mealient.datasource.models.NetworkError
|
||||
import gq.kirmanak.mealient.extensions.checkIfInputIsEmpty
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import gq.kirmanak.mealient.ui.OperationUiState
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
package gq.kirmanak.mealient.ui.recipes.images
|
||||
|
||||
import com.bumptech.glide.load.Options
|
||||
import com.bumptech.glide.load.model.GlideUrl
|
||||
import com.bumptech.glide.load.model.ModelCache
|
||||
import com.bumptech.glide.load.model.ModelLoader
|
||||
import com.bumptech.glide.load.model.*
|
||||
import com.bumptech.glide.load.model.stream.BaseGlideUrlLoader
|
||||
import gq.kirmanak.mealient.data.auth.AuthRepo
|
||||
import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider
|
||||
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
|
||||
import gq.kirmanak.mealient.datasource.DataSourceModule.Companion.AUTHORIZATION_HEADER_NAME
|
||||
import gq.kirmanak.mealient.logging.Logger
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import java.io.InputStream
|
||||
@@ -16,6 +16,7 @@ import javax.inject.Singleton
|
||||
class RecipeModelLoader private constructor(
|
||||
private val recipeImageUrlProvider: RecipeImageUrlProvider,
|
||||
private val logger: Logger,
|
||||
private val authRepo: AuthRepo,
|
||||
concreteLoader: ModelLoader<GlideUrl, InputStream>,
|
||||
cache: ModelCache<RecipeSummaryEntity, GlideUrl>,
|
||||
) : BaseGlideUrlLoader<RecipeSummaryEntity>(concreteLoader, cache) {
|
||||
@@ -24,12 +25,13 @@ class RecipeModelLoader private constructor(
|
||||
class Factory @Inject constructor(
|
||||
private val recipeImageUrlProvider: RecipeImageUrlProvider,
|
||||
private val logger: Logger,
|
||||
private val authRepo: AuthRepo,
|
||||
) {
|
||||
|
||||
fun build(
|
||||
concreteLoader: ModelLoader<GlideUrl, InputStream>,
|
||||
cache: ModelCache<RecipeSummaryEntity, GlideUrl>,
|
||||
) = RecipeModelLoader(recipeImageUrlProvider, logger, concreteLoader, cache)
|
||||
) = RecipeModelLoader(recipeImageUrlProvider, logger, authRepo, concreteLoader, cache)
|
||||
|
||||
}
|
||||
|
||||
@@ -44,4 +46,20 @@ class RecipeModelLoader private constructor(
|
||||
logger.v { "getUrl() called with: model = $model, width = $width, height = $height, options = $options" }
|
||||
return runBlocking { recipeImageUrlProvider.generateImageUrl(model?.slug) }
|
||||
}
|
||||
|
||||
override fun getHeaders(
|
||||
model: RecipeSummaryEntity?,
|
||||
width: Int,
|
||||
height: Int,
|
||||
options: Options?
|
||||
): Headers? {
|
||||
val authorization = runBlocking { authRepo.getAuthHeader() }
|
||||
return if (authorization.isNullOrBlank()) {
|
||||
super.getHeaders(model, width, height, options)
|
||||
} else {
|
||||
LazyHeaders.Builder()
|
||||
.setHeader(AUTHORIZATION_HEADER_NAME, authorization)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package gq.kirmanak.mealient.di
|
||||
|
||||
import dagger.Module
|
||||
import dagger.Provides
|
||||
import dagger.hilt.InstallIn
|
||||
import dagger.hilt.components.SingletonComponent
|
||||
import okhttp3.Interceptor
|
||||
import javax.inject.Singleton
|
||||
|
||||
@Module
|
||||
@InstallIn(SingletonComponent::class)
|
||||
object ReleaseModule {
|
||||
|
||||
// Release version of the application doesn't have any interceptors but this Set
|
||||
// is required by Dagger, so an empty Set is provided here
|
||||
// Use @JvmSuppressWildcards because otherwise dagger can't inject it (https://stackoverflow.com/a/43149382)
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideInterceptors(): Set<@JvmSuppressWildcards Interceptor> = emptySet()
|
||||
}
|
||||
Reference in New Issue
Block a user