From e0a4442e7231cda050b79679b0da6b54a46d9599 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sat, 6 Aug 2022 18:20:23 +0200 Subject: [PATCH 01/13] Create network module --- app/build.gradle.kts | 16 +-- .../mealient/data/add/AddRecipeDataSource.kt | 3 +- .../mealient/data/add/AddRecipeRepo.kt | 2 +- .../data/add/impl/AddRecipeDataSourceImpl.kt | 9 +- .../data/add/impl/AddRecipeRepoImpl.kt | 6 +- .../data/add/impl/AddRecipeService.kt | 12 -- .../mealient/data/auth/AuthDataSource.kt | 2 +- .../data/auth/impl/AuthDataSourceImpl.kt | 45 ++------ .../mealient/data/auth/impl/AuthRepoImpl.kt | 4 +- .../mealient/data/auth/impl/AuthService.kt | 15 --- .../data/baseurl/VersionDataSource.kt | 3 - .../baseurl/impl/VersionDataSourceImpl.kt | 7 +- .../data/baseurl/impl/VersionService.kt | 8 -- .../data/network/AuthenticationInterceptor.kt | 43 ------- .../data/network/MealieDataSourceWrapper.kt | 49 ++++++++ .../mealient/data/network/RetrofitBuilder.kt | 28 ----- .../data/network/RetrofitServiceFactory.kt | 37 ------ .../mealient/data/network/ServiceFactory.kt | 6 - .../mealient/data/recipes/db/RecipeStorage.kt | 4 +- .../data/recipes/db/RecipeStorageImpl.kt | 4 +- .../data/recipes/network/RecipeDataSource.kt | 4 +- .../recipes/network/RecipeDataSourceImpl.kt | 16 +-- .../data/recipes/network/RecipeService.kt | 20 ---- .../kirmanak/mealient/di/AddRecipeModule.kt | 26 ----- .../gq/kirmanak/mealient/di/AuthModule.kt | 23 ---- .../gq/kirmanak/mealient/di/BaseURLModule.kt | 26 ----- .../mealient/di/GlideModuleEntryPoint.kt | 2 - .../gq/kirmanak/mealient/di/NetworkModule.kt | 50 --------- .../gq/kirmanak/mealient/di/RecipeModule.kt | 23 ---- .../mealient/extensions/NetworkExtensions.kt | 20 +--- .../extensions/RemoteToLocalMappings.kt | 31 ++++- .../mealient/ui/add/AddRecipeFragment.kt | 8 +- .../mealient/ui/add/AddRecipeViewModel.kt | 2 +- .../ui/auth/AuthenticationFragment.kt | 2 +- .../mealient/ui/baseurl/BaseURLFragment.kt | 2 +- .../ui/recipes/images/RecipeModelLoader.kt | 26 ++++- datasource/.gitignore | 1 + datasource/build.gradle.kts | 45 ++++++++ .../gq/kirmanak/mealient}/DebugModule.kt | 4 +- .../mealient/datasource/CacheBuilder.kt | 8 ++ .../mealient/datasource/CacheBuilderImpl.kt | 41 +++++++ .../mealient/datasource/DataSourceModule.kt | 68 +++++++++++ .../mealient/datasource/MealieDataSource.kt | 41 +++++++ .../datasource/MealieDataSourceImpl.kt | 106 ++++++++++++++++++ .../mealient/datasource/MealieService.kt | 43 +++++++ .../mealient/datasource/OkHttpBuilder.kt | 8 ++ .../mealient/datasource/OkHttpBuilderImpl.kt | 19 ++++ .../mealient/datasource/RetrofitBuilder.kt | 25 +++++ .../datasource}/models/AddRecipeRequest.kt | 27 +---- .../datasource/models}/ErrorDetail.kt | 2 +- .../models}/GetRecipeIngredientResponse.kt | 2 +- .../models}/GetRecipeInstructionResponse.kt | 2 +- .../datasource/models}/GetRecipeResponse.kt | 2 +- .../models}/GetRecipeSummaryResponse.kt | 2 +- .../datasource/models}/GetTokenResponse.kt | 2 +- .../datasource/models}/NetworkError.kt | 2 +- .../datasource/models}/VersionResponse.kt | 2 +- .../gq/kirmanak/mealient}/ReleaseModule.kt | 2 +- settings.gradle.kts | 1 + 59 files changed, 560 insertions(+), 479 deletions(-) delete mode 100644 app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeService.kt delete mode 100644 app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthService.kt delete mode 100644 app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionService.kt delete mode 100644 app/src/main/java/gq/kirmanak/mealient/data/network/AuthenticationInterceptor.kt create mode 100644 app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt delete mode 100644 app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitBuilder.kt delete mode 100644 app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactory.kt delete mode 100644 app/src/main/java/gq/kirmanak/mealient/data/network/ServiceFactory.kt delete mode 100644 app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeService.kt delete mode 100644 app/src/main/java/gq/kirmanak/mealient/di/NetworkModule.kt create mode 100644 datasource/.gitignore create mode 100644 datasource/build.gradle.kts rename {app/src/debug/java/gq/kirmanak/mealient/di => datasource/src/debug/kotlin/gq/kirmanak/mealient}/DebugModule.kt (94%) create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/CacheBuilder.kt create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/CacheBuilderImpl.kt create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceModule.kt create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSource.kt create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/OkHttpBuilder.kt create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/OkHttpBuilderImpl.kt create mode 100644 datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/RetrofitBuilder.kt rename {app/src/main/java/gq/kirmanak/mealient/data/add => datasource/src/main/kotlin/gq/kirmanak/mealient/datasource}/models/AddRecipeRequest.kt (68%) rename {app/src/main/java/gq/kirmanak/mealient/data/network => datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models}/ErrorDetail.kt (78%) rename {app/src/main/java/gq/kirmanak/mealient/data/recipes/network/response => datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models}/GetRecipeIngredientResponse.kt (88%) rename {app/src/main/java/gq/kirmanak/mealient/data/recipes/network/response => datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models}/GetRecipeInstructionResponse.kt (79%) rename {app/src/main/java/gq/kirmanak/mealient/data/recipes/network/response => datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models}/GetRecipeResponse.kt (94%) rename {app/src/main/java/gq/kirmanak/mealient/data/recipes/network/response => datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models}/GetRecipeSummaryResponse.kt (93%) rename {app/src/main/java/gq/kirmanak/mealient/data/auth/impl => datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models}/GetTokenResponse.kt (79%) rename {app/src/main/java/gq/kirmanak/mealient/data/network => datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models}/NetworkError.kt (87%) rename {app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl => datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models}/VersionResponse.kt (86%) rename {app/src/release/java/gq/kirmanak/mealient/di => datasource/src/release/java/gq/kirmanak/mealient}/ReleaseModule.kt (95%) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cb7990e..00a864f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeDataSource.kt b/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeDataSource.kt index e8390df..1d53c6f 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeDataSource.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeDataSource.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeRepo.kt b/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeRepo.kt index 50c756d..ec59af7 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeRepo.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeRepo.kt @@ -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 { diff --git a/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImpl.kt index 46f7f8c..c687a4d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImpl.kt @@ -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, 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" } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeRepoImpl.kt index 8a0a0a5..2d3c096 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeRepoImpl.kt @@ -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 - 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" } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeService.kt b/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeService.kt deleted file mode 100644 index d59c0d1..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeService.kt +++ /dev/null @@ -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 - -} diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthDataSource.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthDataSource.kt index 576ccab..71ee822 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthDataSource.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/AuthDataSource.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt index 30f4011..53cde84 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt @@ -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, - 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 = logger.logAndMapErrors( - block = { authService.getToken(username = username, password = password) }, - logProvider = { "sendRequest: can't get token" }, - ) - - private fun parseToken( - response: Response - ): String = if (response.isSuccessful) { - response.body()?.accessToken ?: throw NotMealie(NullPointerException("Body is null")) - } else { - val cause = HttpException(response) - val errorDetail = json.runCatching { decodeErrorBody(response) } - .onFailure { logger.e(it) { "Can't decode error body" } } - .getOrNull() - throw when (errorDetail?.detail) { - "Unauthorized" -> Unauthorized(cause) - else -> NotMealie(cause) - } - } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt index 3780a7e..2fc9b2c 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImpl.kt @@ -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) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthService.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthService.kt deleted file mode 100644 index 6fe068b..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthService.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSource.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSource.kt index 9bee6c2..8af39a7 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSource.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSource.kt @@ -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 } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionDataSourceImpl.kt index 62ce4cd..3e8f7df 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionDataSourceImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionDataSourceImpl.kt @@ -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, 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" } ) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionService.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionService.kt deleted file mode 100644 index 4f34f2f..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionService.kt +++ /dev/null @@ -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 -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/AuthenticationInterceptor.kt b/app/src/main/java/gq/kirmanak/mealient/data/network/AuthenticationInterceptor.kt deleted file mode 100644 index c2466df..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/network/AuthenticationInterceptor.kt +++ /dev/null @@ -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" - } -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt b/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt new file mode 100644 index 0000000..319df84 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt @@ -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 { + 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 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 + } + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitBuilder.kt b/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitBuilder.kt deleted file mode 100644 index e36694e..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitBuilder.kt +++ /dev/null @@ -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() - } -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactory.kt b/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactory.kt deleted file mode 100644 index 969fac0..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactory.kt +++ /dev/null @@ -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 RetrofitBuilder.createServiceFactory( - baseURLStorage: BaseURLStorage, - logger: Logger -) = - RetrofitServiceFactory(T::class.java, this, baseURLStorage, logger) - -class RetrofitServiceFactory( - private val serviceClass: Class, - private val retrofitBuilder: RetrofitBuilder, - private val baseURLStorage: BaseURLStorage, - private val logger: Logger, -) : ServiceFactory { - - private val cache: MutableMap = 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 { - logger.v { "createService() called with: url = $url, serviceClass = ${serviceClass.simpleName}" } - val service = retrofitBuilder.buildRetrofit(url).create(serviceClass) - cache[url] = service - return service - } -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/ServiceFactory.kt b/app/src/main/java/gq/kirmanak/mealient/data/network/ServiceFactory.kt deleted file mode 100644 index e46c2a8..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/network/ServiceFactory.kt +++ /dev/null @@ -1,6 +0,0 @@ -package gq.kirmanak.mealient.data.network - -interface ServiceFactory { - - suspend fun provideService(baseUrl: String? = null): T -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt index 382d5a5..0ff12a5 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt @@ -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) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt index 5d75b30..e5d777f 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt @@ -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 diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt index 28e785c..faf9631 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt @@ -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 diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSourceImpl.kt index 1e191f2..626a34d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSourceImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSourceImpl.kt @@ -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, private val logger: Logger, + private val mealieDataSourceWrapper: MealieDataSourceWrapper, ) : RecipeDataSource { override suspend fun requestRecipes(start: Int, limit: Int): List { 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() - } } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeService.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeService.kt deleted file mode 100644 index d21516a..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeService.kt +++ /dev/null @@ -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 - - @GET("/api/recipes/{recipe_slug}") - suspend fun getRecipe( - @Path("recipe_slug") recipeSlug: String, - ): GetRecipeResponse -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/di/AddRecipeModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/AddRecipeModule.kt index 13a92ab..9362d66 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/AddRecipeModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/AddRecipeModule.kt @@ -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 { - return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory( - baseURLStorage, - logger - ) - } - } @Binds @Singleton diff --git a/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt index f7bca5f..4687932 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/AuthModule.kt @@ -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 { - return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory( - baseURLStorage, - logger - ) - } - @Provides @Singleton fun provideAccountManager(@ApplicationContext context: Context): AccountManager { diff --git a/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt index ecb7b2b..4c8d9ce 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt @@ -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 { - return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory( - baseURLStorage, - logger - ) - } - } - @Binds @Singleton fun bindVersionDataSource(versionDataSourceImpl: VersionDataSourceImpl): VersionDataSource diff --git a/app/src/main/java/gq/kirmanak/mealient/di/GlideModuleEntryPoint.kt b/app/src/main/java/gq/kirmanak/mealient/di/GlideModuleEntryPoint.kt index a6f475e..7a4ab32 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/GlideModuleEntryPoint.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/GlideModuleEntryPoint.kt @@ -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 diff --git a/app/src/main/java/gq/kirmanak/mealient/di/NetworkModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/NetworkModule.kt deleted file mode 100644 index 31bad2f..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/di/NetworkModule.kt +++ /dev/null @@ -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 - } -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt index de4090a..adc599d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt @@ -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 { - return RetrofitBuilder(okHttpClient, json, logger).createServiceFactory( - baseURLStorage, - logger - ) - } - @Provides @Singleton fun provideRecipePagingSourceFactory( diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/NetworkExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/NetworkExtensions.kt index 1327d57..7e9a5af 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/NetworkExtensions.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/NetworkExtensions.kt @@ -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 Json.decodeErrorBody(response: Response): 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 Logger.logAndMapErrors( block: () -> T, diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappings.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappings.kt index 0dc2220..f9cf316 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappings.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappings.kt @@ -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) \ No newline at end of file +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, +) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt index 858d5ce..d8807b5 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeFragment.kt @@ -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 diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModel.kt index 4d61ad4..b825eff 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModel.kt @@ -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 diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt index 043189d..651b16f 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationFragment.kt @@ -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 diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt index 0063929..288a2db 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLFragment.kt @@ -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 diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoader.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoader.kt index e1a0487..45a7e4c 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoader.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/images/RecipeModelLoader.kt @@ -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, cache: ModelCache, ) : BaseGlideUrlLoader(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, cache: ModelCache, - ) = 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() + } + } } \ No newline at end of file diff --git a/datasource/.gitignore b/datasource/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/datasource/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/datasource/build.gradle.kts b/datasource/build.gradle.kts new file mode 100644 index 0000000..b702daa --- /dev/null +++ b/datasource/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("gq.kirmanak.mealient.library") + id("kotlin-kapt") + id("dagger.hilt.android.plugin") + id("org.jetbrains.kotlin.plugin.serialization") +} + +android { + defaultConfig { + buildConfigField("Boolean", "LOG_NETWORK", "false") + } + namespace = "gq.kirmanak.mealient.datasource" +} + +dependencies { + implementation(project(":logging")) + + implementation(libs.google.dagger.hiltAndroid) + kapt(libs.google.dagger.hiltCompiler) + kaptTest(libs.google.dagger.hiltAndroidCompiler) + testImplementation(libs.google.dagger.hiltAndroidTesting) + + implementation(libs.jetbrains.kotlinx.datetime) + + implementation(libs.jetbrains.kotlinx.serialization) + + 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.coroutinesAndroid) + testImplementation(libs.jetbrains.kotlinx.coroutinesTest) + + testImplementation(libs.androidx.test.junit) + + testImplementation(libs.google.truth) + + testImplementation(libs.io.mockk) + + debugImplementation(libs.chuckerteam.chucker) +} diff --git a/app/src/debug/java/gq/kirmanak/mealient/di/DebugModule.kt b/datasource/src/debug/kotlin/gq/kirmanak/mealient/DebugModule.kt similarity index 94% rename from app/src/debug/java/gq/kirmanak/mealient/di/DebugModule.kt rename to datasource/src/debug/kotlin/gq/kirmanak/mealient/DebugModule.kt index 62086a0..b7d7c85 100644 --- a/app/src/debug/java/gq/kirmanak/mealient/di/DebugModule.kt +++ b/datasource/src/debug/kotlin/gq/kirmanak/mealient/DebugModule.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.di +package gq.kirmanak.mealient import android.content.Context import com.chuckerteam.chucker.api.ChuckerCollector @@ -10,7 +10,7 @@ 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.datasource.BuildConfig import gq.kirmanak.mealient.logging.Logger import okhttp3.Interceptor import okhttp3.logging.HttpLoggingInterceptor diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/CacheBuilder.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/CacheBuilder.kt new file mode 100644 index 0000000..91aec03 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/CacheBuilder.kt @@ -0,0 +1,8 @@ +package gq.kirmanak.mealient.datasource + +import okhttp3.Cache + +interface CacheBuilder { + + fun buildCache(): Cache +} \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/CacheBuilderImpl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/CacheBuilderImpl.kt new file mode 100644 index 0000000..c2403ed --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/CacheBuilderImpl.kt @@ -0,0 +1,41 @@ +package gq.kirmanak.mealient.datasource + +import android.content.Context +import android.os.StatFs +import dagger.hilt.android.qualifiers.ApplicationContext +import gq.kirmanak.mealient.logging.Logger +import okhttp3.Cache +import java.io.File +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class CacheBuilderImpl @Inject constructor( + @ApplicationContext private val context: Context, + private val logger: Logger, +) : CacheBuilder { + + override fun buildCache(): Cache { + val dir = findCacheDir() + return Cache(dir, calculateDiskCacheSize(dir)) + } + + private fun findCacheDir(): File = File(context.cacheDir, "okhttp") + + private fun calculateDiskCacheSize(dir: File): Long = dir.runCatching { + StatFs(absolutePath).let { + it.blockCountLong * it.blockSizeLong * AVAILABLE_SPACE_PERCENT / 100 + } + } + .onFailure { logger.e(it) { "Can't get available space" } } + .getOrNull() + ?.coerceAtLeast(MIN_OKHTTP_CACHE_SIZE) + ?.coerceAtMost(MAX_OKHTTP_CACHE_SIZE) + ?: MIN_OKHTTP_CACHE_SIZE + + companion object { + private const val MIN_OKHTTP_CACHE_SIZE = 5 * 1024 * 1024L // 5MB + private const val MAX_OKHTTP_CACHE_SIZE = 50 * 1024 * 1024L // 50MB + private const val AVAILABLE_SPACE_PERCENT = 2 + } +} \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceModule.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceModule.kt new file mode 100644 index 0000000..8d29930 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceModule.kt @@ -0,0 +1,68 @@ +package gq.kirmanak.mealient.datasource + +import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory +import dagger.Binds +import dagger.Module +import dagger.Provides +import dagger.hilt.InstallIn +import dagger.hilt.components.SingletonComponent +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import retrofit2.Converter +import retrofit2.Retrofit +import retrofit2.create +import javax.inject.Singleton + +@Module +@InstallIn(SingletonComponent::class) +interface DataSourceModule { + + companion object { + + const val AUTHORIZATION_HEADER_NAME = "Authorization" + + @Provides + @Singleton + fun provideJson(): Json = Json { + coerceInputValues = true + ignoreUnknownKeys = true + encodeDefaults = true + } + + @OptIn(ExperimentalSerializationApi::class) + @Provides + @Singleton + fun provideConverterFactory(json: Json): Converter.Factory = + json.asConverterFactory("application/json".toMediaType()) + + @Provides + @Singleton + fun provideOkHttp(okHttpBuilder: OkHttpBuilder): OkHttpClient = + okHttpBuilder.buildOkHttp() + + @Provides + @Singleton + fun provideRetrofit(retrofitBuilder: RetrofitBuilder): Retrofit = + retrofitBuilder.buildRetrofit("https://beta.mealie.io/") + + @Provides + @Singleton + fun provideMealieService(retrofit: Retrofit): MealieService = + retrofit.create() + + } + + @Binds + @Singleton + fun bindCacheBuilder(cacheBuilderImpl: CacheBuilderImpl): CacheBuilder + + @Binds + @Singleton + fun bindOkHttpBuilder(okHttpBuilderImpl: OkHttpBuilderImpl): OkHttpBuilder + + @Binds + @Singleton + fun bindMealieDataSource(mealientDataSourceImpl: MealieDataSourceImpl): MealieDataSource +} \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSource.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSource.kt new file mode 100644 index 0000000..5cfeb35 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSource.kt @@ -0,0 +1,41 @@ +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 + +interface MealieDataSource { + + suspend fun addRecipe( + baseUrl: String, + token: String?, + recipe: AddRecipeRequest, + ): String + + /** + * Tries to acquire authentication token using the provided credentials + */ + suspend fun authenticate( + baseUrl: String, + username: String, + password: String, + ): String + + suspend fun getVersionInfo( + baseUrl: String, + ): VersionResponse + + suspend fun requestRecipes( + baseUrl: String, + token: String?, + start: Int = 0, + limit: Int = 9999, + ): List + + suspend fun requestRecipeInfo( + baseUrl: String, + token: String?, + slug: String, + ): GetRecipeResponse +} \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt new file mode 100644 index 0000000..1d5a920 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt @@ -0,0 +1,106 @@ +package gq.kirmanak.mealient.datasource + +import gq.kirmanak.mealient.datasource.models.* +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 +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MealieDataSourceImpl @Inject constructor( + private val logger: Logger, + private val mealieService: MealieService, + private val json: Json, +) : MealieDataSource { + + 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() + } + + 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() + } + + 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 requestRecipes( + baseUrl: String, token: String?, start: Int, limit: Int + ): List { + 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() + } + + 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() + } + + private fun parseToken( + response: Response + ): String = if (response.isSuccessful) { + response.body()?.accessToken + ?: throw NetworkError.NotMealie(NullPointerException("Body is null")) + } else { + val cause = HttpException(response) + val errorDetail = json.runCatching { decodeErrorBody(response) } + .onFailure { logger.e(it) { "Can't decode error body" } } + .getOrNull() + throw when (errorDetail?.detail) { + "Unauthorized" -> NetworkError.Unauthorized(cause) + else -> NetworkError.NotMealie(cause) + } + } +} + +private fun Result.getOrThrowUnauthorized(): T = getOrElse { + throw if (it is HttpException && it.code() in listOf(401, 403)) { + NetworkError.Unauthorized(it) + } else { + it + } +} + +@OptIn(ExperimentalSerializationApi::class) +private inline fun Json.decodeErrorBody(response: Response): 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) +} \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt new file mode 100644 index 0000000..72034f8 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt @@ -0,0 +1,43 @@ +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 { + + @FormUrlEncoded + @POST + suspend fun getToken( + @Url url: String, + @Field("username") username: String, + @Field("password") password: String, + ): Response + + @POST + suspend fun addRecipe( + @Url url: String, + @Header(AUTHORIZATION_HEADER_NAME) token: String?, + @Body addRecipeRequest: AddRecipeRequest, + ): String + + @GET + suspend fun getVersion( + @Url url: String, + ): VersionResponse + + @GET + suspend fun getRecipeSummary( + @Url url: String, + @Header(AUTHORIZATION_HEADER_NAME) token: String?, + @Query("start") start: Int, + @Query("limit") limit: Int, + ): List + + @GET + suspend fun getRecipe( + @Url url: String, + @Header(AUTHORIZATION_HEADER_NAME) token: String?, + ): GetRecipeResponse +} \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/OkHttpBuilder.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/OkHttpBuilder.kt new file mode 100644 index 0000000..e31090d --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/OkHttpBuilder.kt @@ -0,0 +1,8 @@ +package gq.kirmanak.mealient.datasource + +import okhttp3.OkHttpClient + +interface OkHttpBuilder { + + fun buildOkHttp(): OkHttpClient +} \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/OkHttpBuilderImpl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/OkHttpBuilderImpl.kt new file mode 100644 index 0000000..f3f5cfd --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/OkHttpBuilderImpl.kt @@ -0,0 +1,19 @@ +package gq.kirmanak.mealient.datasource + +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class OkHttpBuilderImpl @Inject constructor( + private val cacheBuilder: CacheBuilder, + // Use @JvmSuppressWildcards because otherwise dagger can't inject it (https://stackoverflow.com/a/43149382) + private val interceptors: Set<@JvmSuppressWildcards Interceptor>, +) : OkHttpBuilder { + + override fun buildOkHttp(): OkHttpClient = OkHttpClient.Builder() + .apply { interceptors.forEach(::addNetworkInterceptor) } + .cache(cacheBuilder.buildCache()) + .build() +} \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/RetrofitBuilder.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/RetrofitBuilder.kt new file mode 100644 index 0000000..fe6b26e --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/RetrofitBuilder.kt @@ -0,0 +1,25 @@ +package gq.kirmanak.mealient.datasource + +import gq.kirmanak.mealient.logging.Logger +import okhttp3.OkHttpClient +import retrofit2.Converter.Factory +import retrofit2.Retrofit +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class RetrofitBuilder @Inject constructor( + private val okHttpClient: OkHttpClient, + private val converterFactory: Factory, + private val logger: Logger, +) { + + fun buildRetrofit(baseUrl: String): Retrofit { + logger.v { "buildRetrofit() called with: baseUrl = $baseUrl" } + return Retrofit.Builder() + .baseUrl(baseUrl) + .client(okHttpClient) + .addConverterFactory(converterFactory) + .build() + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/add/models/AddRecipeRequest.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/AddRecipeRequest.kt similarity index 68% rename from app/src/main/java/gq/kirmanak/mealient/data/add/models/AddRecipeRequest.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/AddRecipeRequest.kt index 08307af..9c2f34b 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/add/models/AddRecipeRequest.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/AddRecipeRequest.kt @@ -1,6 +1,5 @@ -package gq.kirmanak.mealient.data.add.models +package gq.kirmanak.mealient.datasource.models -import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -20,29 +19,7 @@ data class AddRecipeRequest( @SerialName("extras") val extras: Map = emptyMap(), @SerialName("assets") val assets: List = 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( diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/ErrorDetail.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/ErrorDetail.kt similarity index 78% rename from app/src/main/java/gq/kirmanak/mealient/data/network/ErrorDetail.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/ErrorDetail.kt index 674de44..00efd12 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/network/ErrorDetail.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/ErrorDetail.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.data.network +package gq.kirmanak.mealient.datasource.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/response/GetRecipeIngredientResponse.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeIngredientResponse.kt similarity index 88% rename from app/src/main/java/gq/kirmanak/mealient/data/recipes/network/response/GetRecipeIngredientResponse.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeIngredientResponse.kt index 090c00a..e00a9fb 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/response/GetRecipeIngredientResponse.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeIngredientResponse.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.data.recipes.network.response +package gq.kirmanak.mealient.datasource.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/response/GetRecipeInstructionResponse.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeInstructionResponse.kt similarity index 79% rename from app/src/main/java/gq/kirmanak/mealient/data/recipes/network/response/GetRecipeInstructionResponse.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeInstructionResponse.kt index edbedce..c6c2fe7 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/response/GetRecipeInstructionResponse.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeInstructionResponse.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.data.recipes.network.response +package gq.kirmanak.mealient.datasource.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/response/GetRecipeResponse.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeResponse.kt similarity index 94% rename from app/src/main/java/gq/kirmanak/mealient/data/recipes/network/response/GetRecipeResponse.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeResponse.kt index 11df57c..3197d75 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/response/GetRecipeResponse.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeResponse.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.data.recipes.network.response +package gq.kirmanak.mealient.datasource.models import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/response/GetRecipeSummaryResponse.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeSummaryResponse.kt similarity index 93% rename from app/src/main/java/gq/kirmanak/mealient/data/recipes/network/response/GetRecipeSummaryResponse.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeSummaryResponse.kt index c5349a9..f9fc613 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/response/GetRecipeSummaryResponse.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeSummaryResponse.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.data.recipes.network.response +package gq.kirmanak.mealient.datasource.models import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/GetTokenResponse.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetTokenResponse.kt similarity index 79% rename from app/src/main/java/gq/kirmanak/mealient/data/auth/impl/GetTokenResponse.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetTokenResponse.kt index 029e76e..66a79db 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/GetTokenResponse.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetTokenResponse.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.data.auth.impl +package gq.kirmanak.mealient.datasource.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/NetworkError.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/NetworkError.kt similarity index 87% rename from app/src/main/java/gq/kirmanak/mealient/data/network/NetworkError.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/NetworkError.kt index 05ef278..7fcd669 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/network/NetworkError.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/NetworkError.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.data.network +package gq.kirmanak.mealient.datasource.models sealed class NetworkError(cause: Throwable) : RuntimeException(cause) { class Unauthorized(cause: Throwable) : NetworkError(cause) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionResponse.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/VersionResponse.kt similarity index 86% rename from app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionResponse.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/VersionResponse.kt index 3c7efe7..44f0a09 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionResponse.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/VersionResponse.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.data.baseurl.impl +package gq.kirmanak.mealient.datasource.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/release/java/gq/kirmanak/mealient/di/ReleaseModule.kt b/datasource/src/release/java/gq/kirmanak/mealient/ReleaseModule.kt similarity index 95% rename from app/src/release/java/gq/kirmanak/mealient/di/ReleaseModule.kt rename to datasource/src/release/java/gq/kirmanak/mealient/ReleaseModule.kt index 40b35e3..1be726e 100644 --- a/app/src/release/java/gq/kirmanak/mealient/di/ReleaseModule.kt +++ b/datasource/src/release/java/gq/kirmanak/mealient/ReleaseModule.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.di +package gq.kirmanak.mealient import dagger.Module import dagger.Provides diff --git a/settings.gradle.kts b/settings.gradle.kts index a45a879..3c9d35e 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -23,3 +23,4 @@ include(":app") include(":database") include(":datastore") include(":logging") +include(":datasource") From 35566d8fa9bfb54318925758076580e4c9179704 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 7 Aug 2022 11:00:19 +0200 Subject: [PATCH 02/13] Hide MealieDataSourceWrapper behind interfaces --- .../data/add/impl/AddRecipeDataSourceImpl.kt | 26 ------------ .../baseurl/impl/VersionDataSourceImpl.kt | 28 ------------- .../data/network/MealieDataSourceWrapper.kt | 40 +++++++++---------- .../recipes/network/RecipeDataSourceImpl.kt | 30 -------------- .../kirmanak/mealient/di/AddRecipeModule.kt | 4 +- .../gq/kirmanak/mealient/di/BaseURLModule.kt | 4 +- .../gq/kirmanak/mealient/di/RecipeModule.kt | 4 +- .../extensions/RemoteToLocalMappings.kt | 2 +- 8 files changed, 27 insertions(+), 111 deletions(-) delete mode 100644 app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImpl.kt delete mode 100644 app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionDataSourceImpl.kt delete mode 100644 app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSourceImpl.kt diff --git a/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImpl.kt deleted file mode 100644 index c687a4d..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImpl.kt +++ /dev/null @@ -1,26 +0,0 @@ -package gq.kirmanak.mealient.data.add.impl - -import gq.kirmanak.mealient.data.add.AddRecipeDataSource -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 -import javax.inject.Singleton - -@Singleton -class AddRecipeDataSourceImpl @Inject constructor( - private val logger: Logger, - private val mealieDataSourceWrapper: MealieDataSourceWrapper, -) : AddRecipeDataSource { - - override suspend fun addRecipe(recipe: AddRecipeRequest): String { - logger.v { "addRecipe() called with: recipe = $recipe" } - val response = logger.logAndMapErrors( - block = { mealieDataSourceWrapper.addRecipe(recipe) }, - logProvider = { "addRecipe: can't add recipe" } - ) - logger.v { "addRecipe() response = $response" } - return response - } -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionDataSourceImpl.kt deleted file mode 100644 index 3e8f7df..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionDataSourceImpl.kt +++ /dev/null @@ -1,28 +0,0 @@ -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.MealieDataSourceWrapper -import gq.kirmanak.mealient.extensions.logAndMapErrors -import gq.kirmanak.mealient.extensions.versionInfo -import gq.kirmanak.mealient.logging.Logger -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class VersionDataSourceImpl @Inject constructor( - private val logger: Logger, - private val mealieDataSourceWrapper: MealieDataSourceWrapper, -) : VersionDataSource { - - override suspend fun getVersionInfo(baseUrl: String): VersionInfo { - logger.v { "getVersionInfo() called with: baseUrl = $baseUrl" } - - val response = logger.logAndMapErrors( - block = { mealieDataSourceWrapper.getVersionInfo(baseUrl) }, - logProvider = { "getVersionInfo: can't request version" } - ) - - return response.versionInfo() - } -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt b/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt index 319df84..4bb6a63 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt @@ -1,10 +1,17 @@ package gq.kirmanak.mealient.data.network +import gq.kirmanak.mealient.data.add.AddRecipeDataSource import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.baseurl.BaseURLStorage +import gq.kirmanak.mealient.data.baseurl.VersionDataSource +import gq.kirmanak.mealient.data.baseurl.VersionInfo +import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource import gq.kirmanak.mealient.datasource.MealieDataSource -import gq.kirmanak.mealient.datasource.models.* +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.NetworkError +import gq.kirmanak.mealient.extensions.toVersionInfo import javax.inject.Inject import javax.inject.Singleton @@ -13,34 +20,27 @@ class MealieDataSourceWrapper @Inject constructor( private val baseURLStorage: BaseURLStorage, private val authRepo: AuthRepo, private val mealieDataSource: MealieDataSource, -) { +) : AddRecipeDataSource, RecipeDataSource, VersionDataSource { - suspend fun addRecipe(recipe: AddRecipeRequest): String { - val baseUrl = baseURLStorage.requireBaseURL() - return withAuthHeader { token -> addRecipe(baseUrl, token, recipe) } - } + override suspend fun addRecipe(recipe: AddRecipeRequest): String = + withAuthHeader { token -> addRecipe(getUrl(), token, recipe) } - suspend fun getVersionInfo(baseUrl: String): VersionResponse { - return mealieDataSource.getVersionInfo(baseUrl) - } + override suspend fun getVersionInfo(baseUrl: String): VersionInfo = + mealieDataSource.getVersionInfo(baseUrl).toVersionInfo() - suspend fun requestRecipes( - start: Int = 0, - limit: Int = 9999, - ): List { - val baseUrl = baseURLStorage.requireBaseURL() - return withAuthHeader { token -> requestRecipes(baseUrl, token, start, limit) } - } + override suspend fun requestRecipes(start: Int, limit: Int): List = + withAuthHeader { token -> requestRecipes(getUrl(), token, start, limit) } - suspend fun requestRecipeInfo(slug: String): GetRecipeResponse { - val baseUrl = baseURLStorage.requireBaseURL() - return withAuthHeader { token -> requestRecipeInfo(baseUrl, token, slug) } - } + override suspend fun requestRecipeInfo(slug: String): GetRecipeResponse = + withAuthHeader { token -> requestRecipeInfo(getUrl(), token, slug) } + + private suspend fun getUrl() = baseURLStorage.requireBaseURL() private suspend inline fun withAuthHeader(block: MealieDataSource.(String?) -> T): T = mealieDataSource.runCatching { block(authRepo.getAuthHeader()) }.getOrElse { if (it is NetworkError.Unauthorized) { authRepo.invalidateAuthHeader() + // Trying again with new authentication header mealieDataSource.block(authRepo.getAuthHeader()) } else { throw it diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSourceImpl.kt deleted file mode 100644 index 626a34d..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSourceImpl.kt +++ /dev/null @@ -1,30 +0,0 @@ -package gq.kirmanak.mealient.data.recipes.network - -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 logger: Logger, - private val mealieDataSourceWrapper: MealieDataSourceWrapper, -) : RecipeDataSource { - - override suspend fun requestRecipes(start: Int, limit: Int): List { - logger.v { "requestRecipes() called with: start = $start, limit = $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 = mealieDataSourceWrapper.requestRecipeInfo(slug) - logger.v { "requestRecipeInfo() returned: $recipeInfo" } - return recipeInfo - } - -} diff --git a/app/src/main/java/gq/kirmanak/mealient/di/AddRecipeModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/AddRecipeModule.kt index 9362d66..97f9ce8 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/AddRecipeModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/AddRecipeModule.kt @@ -6,8 +6,8 @@ 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.network.MealieDataSourceWrapper import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorage import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorageImpl import javax.inject.Singleton @@ -23,7 +23,7 @@ interface AddRecipeModule { @Binds @Singleton - fun bindAddRecipeDataSource(addRecipeDataSourceImpl: AddRecipeDataSourceImpl): AddRecipeDataSource + fun bindAddRecipeDataSource(mealieDataSourceWrapper: MealieDataSourceWrapper): AddRecipeDataSource @Binds @Singleton diff --git a/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt index 4c8d9ce..30300c3 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt @@ -7,7 +7,7 @@ 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.network.MealieDataSourceWrapper import javax.inject.Singleton @Module @@ -16,7 +16,7 @@ interface BaseURLModule { @Binds @Singleton - fun bindVersionDataSource(versionDataSourceImpl: VersionDataSourceImpl): VersionDataSource + fun bindVersionDataSource(mealieDataSourceWrapper: MealieDataSourceWrapper): VersionDataSource @Binds @Singleton diff --git a/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt b/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt index adc599d..8e6213d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/RecipeModule.kt @@ -9,6 +9,7 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent import gq.kirmanak.mealient.R +import gq.kirmanak.mealient.data.network.MealieDataSourceWrapper import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.data.recipes.db.RecipeStorageImpl @@ -16,7 +17,6 @@ import gq.kirmanak.mealient.data.recipes.impl.RecipeImageUrlProvider 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.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.ui.recipes.images.RecipeModelLoaderFactory import java.io.InputStream @@ -28,7 +28,7 @@ interface RecipeModule { @Binds @Singleton - fun provideRecipeDataSource(recipeDataSourceImpl: RecipeDataSourceImpl): RecipeDataSource + fun provideRecipeDataSource(mealieDataSourceWrapper: MealieDataSourceWrapper): RecipeDataSource @Binds @Singleton diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappings.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappings.kt index f9cf316..2cd87a3 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappings.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappings.kt @@ -42,7 +42,7 @@ fun GetRecipeSummaryResponse.recipeEntity() = RecipeSummaryEntity( dateUpdated = dateUpdated, ) -fun VersionResponse.versionInfo() = VersionInfo(production, version, demoStatus) +fun VersionResponse.toVersionInfo() = VersionInfo(production, version, demoStatus) fun AddRecipeDraft.toAddRecipeRequest() = AddRecipeRequest( name = recipeName, From 725b75211d124cba7da57a937a697bc53ec9ca70 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 7 Aug 2022 11:42:36 +0200 Subject: [PATCH 03/13] Simplify MealieDataSourceImpl --- .../data/auth/impl/AuthDataSourceImpl.kt | 12 +- .../mealient/extensions/NetworkExtensions.kt | 12 -- .../datasource/MealieDataSourceImpl.kt | 108 ++++++++---------- .../mealient/datasource/MealieService.kt | 3 +- 4 files changed, 49 insertions(+), 86 deletions(-) delete mode 100644 app/src/main/java/gq/kirmanak/mealient/extensions/NetworkExtensions.kt diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt index 53cde84..5721c80 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt @@ -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) } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/NetworkExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/NetworkExtensions.kt deleted file mode 100644 index 7e9a5af..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/NetworkExtensions.kt +++ /dev/null @@ -1,12 +0,0 @@ -package gq.kirmanak.mealient.extensions - -import gq.kirmanak.mealient.datasource.mapToNetworkError -import gq.kirmanak.mealient.logging.Logger - -inline fun Logger.logAndMapErrors( - block: () -> T, - noinline logProvider: () -> String -): T = runCatchingExceptCancel(block).getOrElse { - e(it, messageSupplier = logProvider) - throw it.mapToNetworkError() -} \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt index 1d5a920..114e784 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt @@ -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() + 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 { - 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 = + 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 makeCall( + crossinline block: suspend MealieService.() -> T, + crossinline logMethod: () -> String, + crossinline logParameters: () -> String, + ): Result { + 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 - ): String = if (response.isSuccessful) { - response.body()?.accessToken - ?: throw NetworkError.NotMealie(NullPointerException("Body is null")) - } else { - val cause = HttpException(response) - val errorDetail = json.runCatching { 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 ResponseBody.decode(): R = json.decodeFromStream(byteStream()) } private fun Result.getOrThrowUnauthorized(): T = getOrElse { @@ -92,15 +87,4 @@ private fun Result.getOrThrowUnauthorized(): T = getOrElse { } else { it } -} - -@OptIn(ExperimentalSerializationApi::class) -private inline fun Json.decodeErrorBody(response: Response): 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) } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt index 72034f8..9750cf9 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt @@ -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 @POST suspend fun addRecipe( From 086368e417e58309d2f83e95184d00c3a2ac9a8a Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 7 Aug 2022 11:45:40 +0200 Subject: [PATCH 04/13] Improve getVersionInfo error handling --- .../gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt index 114e784..527fef5 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt @@ -8,6 +8,8 @@ import kotlinx.serialization.json.Json import kotlinx.serialization.json.decodeFromStream import okhttp3.ResponseBody import retrofit2.HttpException +import java.net.ConnectException +import java.net.SocketTimeoutException import javax.inject.Inject import javax.inject.Singleton @@ -45,6 +47,7 @@ class MealieDataSourceImpl @Inject constructor( ).getOrElse { throw when (it) { is HttpException, is SerializationException -> NetworkError.NotMealie(it) + is SocketTimeoutException, is ConnectException -> NetworkError.NoServerConnection(it) else -> NetworkError.MalformedUrl(it) } } From ec10eab888347ab68a1595491c1919ae55f55d84 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 7 Aug 2022 11:52:27 +0200 Subject: [PATCH 05/13] Remove unused default parameters --- .../mealient/data/recipes/network/RecipeDataSource.kt | 2 +- .../gq/kirmanak/mealient/datasource/MealieDataSource.kt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt index faf9631..5e82886 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt @@ -4,7 +4,7 @@ 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 + suspend fun requestRecipes(start: Int, limit: Int): List suspend fun requestRecipeInfo(slug: String): GetRecipeResponse } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSource.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSource.kt index 5cfeb35..c8fd1ae 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSource.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSource.kt @@ -29,8 +29,8 @@ interface MealieDataSource { suspend fun requestRecipes( baseUrl: String, token: String?, - start: Int = 0, - limit: Int = 9999, + start: Int, + limit: Int, ): List suspend fun requestRecipeInfo( From c810e0550ab138ddab3b163f3a95183f1108a580 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 7 Aug 2022 12:00:29 +0200 Subject: [PATCH 06/13] Fix RemoteToLocalMappingsTest compilation --- .run/Run tests.run.xml | 24 +++++++++ .../data/auth/impl/AuthDataSourceImpl.kt | 2 - .../add/impl/AddRecipeDataSourceImplTest.kt | 50 ------------------- .../RemoteToLocalMappingsTest.kt} | 14 ++++-- 4 files changed, 33 insertions(+), 57 deletions(-) create mode 100644 .run/Run tests.run.xml delete mode 100644 app/src/test/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImplTest.kt rename app/src/test/java/gq/kirmanak/mealient/{data/add/models/AddRecipeRequestTest.kt => extensions/RemoteToLocalMappingsTest.kt} (82%) diff --git a/.run/Run tests.run.xml b/.run/Run tests.run.xml new file mode 100644 index 0000000..fc738b6 --- /dev/null +++ b/.run/Run tests.run.xml @@ -0,0 +1,24 @@ + + + + + + + false + true + false + + + \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt index 5721c80..7cadcc0 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImpl.kt @@ -2,13 +2,11 @@ package gq.kirmanak.mealient.data.auth.impl import gq.kirmanak.mealient.data.auth.AuthDataSource import gq.kirmanak.mealient.datasource.MealieDataSource -import gq.kirmanak.mealient.logging.Logger import javax.inject.Inject import javax.inject.Singleton @Singleton class AuthDataSourceImpl @Inject constructor( - private val logger: Logger, private val mealieDataSource: MealieDataSource, ) : AuthDataSource { diff --git a/app/src/test/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImplTest.kt deleted file mode 100644 index f58334e..0000000 --- a/app/src/test/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImplTest.kt +++ /dev/null @@ -1,50 +0,0 @@ -package gq.kirmanak.mealient.data.add.impl - -import com.google.common.truth.Truth.assertThat -import gq.kirmanak.mealient.data.add.models.AddRecipeRequest -import gq.kirmanak.mealient.data.network.NetworkError -import gq.kirmanak.mealient.data.network.ServiceFactory -import gq.kirmanak.mealient.logging.Logger -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.impl.annotations.MockK -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import kotlinx.serialization.SerializationException -import org.junit.Before -import org.junit.Test - -@OptIn(ExperimentalCoroutinesApi::class) -class AddRecipeDataSourceImplTest { - - @MockK - lateinit var serviceProvider: ServiceFactory - - @MockK - lateinit var service: AddRecipeService - - @MockK(relaxUnitFun = true) - lateinit var logger: Logger - - lateinit var subject: AddRecipeDataSourceImpl - - @Before - fun setUp() { - MockKAnnotations.init(this) - coEvery { serviceProvider.provideService(any()) } returns service - subject = AddRecipeDataSourceImpl(serviceProvider, logger) - } - - @Test(expected = NetworkError.NotMealie::class) - fun `when addRecipe fails then maps error`() = runTest { - coEvery { service.addRecipe(any()) } throws SerializationException() - subject.addRecipe(AddRecipeRequest()) - } - - @Test - fun `when addRecipe succeeds then returns response`() = runTest { - coEvery { service.addRecipe(any()) } returns "response" - assertThat(subject.addRecipe(AddRecipeRequest())).isEqualTo("response") - } - -} \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/data/add/models/AddRecipeRequestTest.kt b/app/src/test/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappingsTest.kt similarity index 82% rename from app/src/test/java/gq/kirmanak/mealient/data/add/models/AddRecipeRequestTest.kt rename to app/src/test/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappingsTest.kt index 78ab001..56b8428 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/add/models/AddRecipeRequestTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappingsTest.kt @@ -1,13 +1,17 @@ -package gq.kirmanak.mealient.data.add.models +package gq.kirmanak.mealient.extensions import com.google.common.truth.Truth.assertThat +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.datastore.recipe.AddRecipeDraft import org.junit.Test -class AddRecipeRequestTest { +class RemoteToLocalMappingsTest { @Test - fun `when construct from input then fills fields correctly`() { + fun `when toAddRecipeRequest then fills fields correctly`() { val input = AddRecipeDraft( recipeName = "Recipe name", recipeDescription = "Recipe description", @@ -36,11 +40,11 @@ class AddRecipeRequestTest { ) ) - assertThat(AddRecipeRequest(input)).isEqualTo(expected) + assertThat(input.toAddRecipeRequest()).isEqualTo(expected) } @Test - fun `when toInput then fills fields correctly`() { + fun `when toDraft then fills fields correctly`() { val request = AddRecipeRequest( name = "Recipe name", description = "Recipe description", From c2749d629e0e32848d31818edd3bee37d892bdf9 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 7 Aug 2022 12:26:48 +0200 Subject: [PATCH 07/13] Move VersionDataSourceImplTest --- .../data/baseurl/VersionDataSourceImplTest.kt | 80 ------------------- .../datasource/MealieDataSourceImplTest.kt | 69 ++++++++++++++++ .../mealient/datasource}/TestExtensions.kt | 0 3 files changed, 69 insertions(+), 80 deletions(-) delete mode 100644 app/src/test/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImplTest.kt create mode 100644 datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImplTest.kt rename {app/src/test/java/gq/kirmanak/mealient/test => datasource/src/test/kotlin/gq/kirmanak/mealient/datasource}/TestExtensions.kt (100%) diff --git a/app/src/test/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImplTest.kt deleted file mode 100644 index 54df6a5..0000000 --- a/app/src/test/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImplTest.kt +++ /dev/null @@ -1,80 +0,0 @@ -package gq.kirmanak.mealient.data.baseurl - -import com.google.common.truth.Truth.assertThat -import gq.kirmanak.mealient.data.baseurl.impl.VersionDataSourceImpl -import gq.kirmanak.mealient.data.baseurl.impl.VersionResponse -import gq.kirmanak.mealient.data.baseurl.impl.VersionService -import gq.kirmanak.mealient.data.network.NetworkError -import gq.kirmanak.mealient.data.network.ServiceFactory -import gq.kirmanak.mealient.logging.Logger -import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL -import gq.kirmanak.mealient.test.toJsonResponseBody -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.impl.annotations.MockK -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import kotlinx.serialization.SerializationException -import okio.IOException -import org.junit.Before -import org.junit.Test -import retrofit2.HttpException -import retrofit2.Response - -@OptIn(ExperimentalCoroutinesApi::class) -class VersionDataSourceImplTest { - @MockK - lateinit var versionService: VersionService - - @MockK - lateinit var versionServiceFactory: ServiceFactory - - @MockK(relaxUnitFun = true) - lateinit var logger: Logger - - lateinit var subject: VersionDataSource - - @Before - fun setUp() { - MockKAnnotations.init(this) - subject = VersionDataSourceImpl(versionServiceFactory, logger) - coEvery { versionServiceFactory.provideService(eq(TEST_BASE_URL)) } returns versionService - } - - @Test(expected = NetworkError.MalformedUrl::class) - fun `when getVersionInfo and provideService throws then MalformedUrl`() = runTest { - coEvery { - versionServiceFactory.provideService(eq(TEST_BASE_URL)) - } throws NetworkError.MalformedUrl(RuntimeException()) - subject.getVersionInfo(TEST_BASE_URL) - } - - @Test(expected = NetworkError.NotMealie::class) - fun `when getVersionInfo and getVersion throws HttpException then NotMealie`() = runTest { - val error = HttpException(Response.error(404, "".toJsonResponseBody())) - coEvery { versionService.getVersion() } throws error - subject.getVersionInfo(TEST_BASE_URL) - } - - @Test(expected = NetworkError.NotMealie::class) - fun `when getVersionInfo and getVersion throws SerializationException then NotMealie`() = - runTest { - coEvery { versionService.getVersion() } throws SerializationException() - subject.getVersionInfo(TEST_BASE_URL) - } - - @Test(expected = NetworkError.NoServerConnection::class) - fun `when getVersionInfo and getVersion throws IOException then NoServerConnection`() = - runTest { - coEvery { versionService.getVersion() } throws IOException() - subject.getVersionInfo(TEST_BASE_URL) - } - - @Test - fun `when getVersionInfo and getVersion returns result then result`() = runTest { - coEvery { versionService.getVersion() } returns VersionResponse(true, "v0.5.6", true) - assertThat(subject.getVersionInfo(TEST_BASE_URL)).isEqualTo( - VersionInfo(true, "v0.5.6", true) - ) - } -} \ No newline at end of file diff --git a/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImplTest.kt b/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImplTest.kt new file mode 100644 index 0000000..2ec745e --- /dev/null +++ b/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImplTest.kt @@ -0,0 +1,69 @@ +package gq.kirmanak.mealient.datasource + +import com.google.common.truth.Truth.assertThat +import gq.kirmanak.mealient.datasource.models.NetworkError +import gq.kirmanak.mealient.datasource.models.VersionResponse +import gq.kirmanak.mealient.logging.Logger +import gq.kirmanak.mealient.test.toJsonResponseBody +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import org.junit.Before +import org.junit.Test +import retrofit2.HttpException +import retrofit2.Response +import java.net.ConnectException + +@OptIn(ExperimentalCoroutinesApi::class) +class MealieDataSourceImplTest { + + @MockK + lateinit var service: MealieService + + @MockK(relaxUnitFun = true) + lateinit var logger: Logger + + lateinit var subject: MealieDataSourceImpl + + @Before + fun setUp() { + MockKAnnotations.init(this) + subject = MealieDataSourceImpl(logger, service, Json.Default) + } + + @Test(expected = NetworkError.NotMealie::class) + fun `when getVersionInfo and getVersion throws HttpException then NotMealie`() = runTest { + val error = HttpException(Response.error(404, "".toJsonResponseBody())) + coEvery { service.getVersion(any()) } throws error + subject.getVersionInfo(TEST_BASE_URL) + } + + @Test(expected = NetworkError.NotMealie::class) + fun `when getVersionInfo and getVersion throws SerializationException then NotMealie`() = + runTest { + coEvery { service.getVersion(any()) } throws SerializationException() + subject.getVersionInfo(TEST_BASE_URL) + } + + @Test(expected = NetworkError.NoServerConnection::class) + fun `when getVersionInfo and getVersion throws IOException then NoServerConnection`() = + runTest { + coEvery { service.getVersion(any()) } throws ConnectException() + subject.getVersionInfo(TEST_BASE_URL) + } + + @Test + fun `when getVersionInfo and getVersion returns result then result`() = runTest { + val versionResponse = VersionResponse(true, "v0.5.6", true) + coEvery { service.getVersion(any()) } returns versionResponse + assertThat(subject.getVersionInfo(TEST_BASE_URL)).isSameInstanceAs(versionResponse) + } + + companion object { + private const val TEST_BASE_URL = "" + } +} \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/test/TestExtensions.kt b/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/TestExtensions.kt similarity index 100% rename from app/src/test/java/gq/kirmanak/mealient/test/TestExtensions.kt rename to datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/TestExtensions.kt From 27db0520e35047f137d117f1a1b4e0ef525249f8 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 7 Aug 2022 12:38:48 +0200 Subject: [PATCH 08/13] Move AuthDataSourceImplTest --- .../data/auth/impl/AuthDataSourceImplTest.kt | 91 ------------------- .../datasource/MealieDataSourceImplTest.kt | 50 +++++++++- 2 files changed, 49 insertions(+), 92 deletions(-) delete mode 100644 app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImplTest.kt diff --git a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImplTest.kt deleted file mode 100644 index 23b99dc..0000000 --- a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthDataSourceImplTest.kt +++ /dev/null @@ -1,91 +0,0 @@ -package gq.kirmanak.mealient.data.auth.impl - -import com.google.common.truth.Truth.assertThat -import gq.kirmanak.mealient.data.network.NetworkError.* -import gq.kirmanak.mealient.data.network.ServiceFactory -import gq.kirmanak.mealient.di.NetworkModule -import gq.kirmanak.mealient.logging.Logger -import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD -import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN -import gq.kirmanak.mealient.test.AuthImplTestData.TEST_USERNAME -import gq.kirmanak.mealient.test.toJsonResponseBody -import io.mockk.MockKAnnotations -import io.mockk.coEvery -import io.mockk.impl.annotations.MockK -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import retrofit2.Response -import java.io.IOException - -@OptIn(ExperimentalCoroutinesApi::class) -class AuthDataSourceImplTest { - @MockK - lateinit var authService: AuthService - - @MockK - lateinit var authServiceFactory: ServiceFactory - - @MockK(relaxUnitFun = true) - lateinit var logger: Logger - - lateinit var subject: AuthDataSourceImpl - - @Before - fun setUp() { - MockKAnnotations.init(this) - subject = AuthDataSourceImpl(authServiceFactory, NetworkModule.createJson(), logger) - coEvery { authServiceFactory.provideService(any()) } returns authService - } - - @Test - fun `when authentication is successful then token is correct`() = runTest { - val token = authenticate(Response.success(GetTokenResponse(TEST_TOKEN))) - assertThat(token).isEqualTo(TEST_TOKEN) - } - - @Test(expected = Unauthorized::class) - fun `when authenticate receives 401 and Unauthorized then throws Unauthorized`() = runTest { - val body = "{\"detail\":\"Unauthorized\"}".toJsonResponseBody() - authenticate(Response.error(401, body)) - } - - @Test(expected = NotMealie::class) - fun `when authenticate receives 401 but not Unauthorized then throws NotMealie`() = runTest { - val body = "{\"detail\":\"Something\"}".toJsonResponseBody() - authenticate(Response.error(401, body)) - } - - @Test(expected = NotMealie::class) - fun `when authenticate receives 404 and empty body then throws NotMealie`() = runTest { - authenticate(Response.error(401, "".toJsonResponseBody())) - } - - @Test(expected = NotMealie::class) - fun `when authenticate receives 200 and null then throws NotMealie`() = runTest { - authenticate(Response.success(200, null)) - } - - @Test(expected = NoServerConnection::class) - fun `when authenticate and getToken throws then throws NoServerConnection`() = runTest { - coEvery { authService.getToken(any(), any()) } throws IOException("Server not found") - callAuthenticate() - } - - @Test(expected = MalformedUrl::class) - fun `when authenticate and provideService throws then MalformedUrl`() = runTest { - coEvery { - authServiceFactory.provideService(any()) - } throws MalformedUrl(RuntimeException()) - callAuthenticate() - } - - private suspend fun authenticate(response: Response): String { - coEvery { authService.getToken(eq(TEST_USERNAME), eq(TEST_PASSWORD)) } returns response - return callAuthenticate() - } - - private suspend fun callAuthenticate() = subject.authenticate(TEST_USERNAME, TEST_PASSWORD) - -} \ No newline at end of file diff --git a/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImplTest.kt b/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImplTest.kt index 2ec745e..fa82b9b 100644 --- a/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImplTest.kt +++ b/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImplTest.kt @@ -1,6 +1,7 @@ package gq.kirmanak.mealient.datasource import com.google.common.truth.Truth.assertThat +import gq.kirmanak.mealient.datasource.models.GetTokenResponse import gq.kirmanak.mealient.datasource.models.NetworkError import gq.kirmanak.mealient.datasource.models.VersionResponse import gq.kirmanak.mealient.logging.Logger @@ -16,6 +17,7 @@ import org.junit.Before import org.junit.Test import retrofit2.HttpException import retrofit2.Response +import java.io.IOException import java.net.ConnectException @OptIn(ExperimentalCoroutinesApi::class) @@ -63,7 +65,53 @@ class MealieDataSourceImplTest { assertThat(subject.getVersionInfo(TEST_BASE_URL)).isSameInstanceAs(versionResponse) } + @Test + fun `when authentication is successful then token is correct`() = runTest { + coEvery { service.getToken(any(), any(), any()) } returns GetTokenResponse(TEST_TOKEN) + assertThat(callAuthenticate()).isEqualTo(TEST_TOKEN) + } + + @Test(expected = NetworkError.Unauthorized::class) + fun `when authenticate receives 401 and Unauthorized then throws Unauthorized`() = runTest { + val body = "{\"detail\":\"Unauthorized\"}".toJsonResponseBody() + coEvery { + service.getToken(any(), any(), any()) + } throws HttpException(Response.error(401, body)) + callAuthenticate() + } + + @Test(expected = HttpException::class) + fun `when authenticate receives 401 but not Unauthorized then throws NotMealie`() = runTest { + val body = "{\"detail\":\"Something\"}".toJsonResponseBody() + coEvery { + service.getToken(any(), any(), any()) + } throws HttpException(Response.error(401, body)) + callAuthenticate() + } + + @Test(expected = SerializationException::class) + fun `when authenticate receives 404 and empty body then throws NotMealie`() = runTest { + val body = "".toJsonResponseBody() + coEvery { + service.getToken(any(), any(), any()) + } throws HttpException(Response.error(401, body)) + callAuthenticate() + } + + @Test(expected = IOException::class) + fun `when authenticate and getToken throws then throws NoServerConnection`() = runTest { + coEvery { service.getToken(any(), any(), any()) } throws IOException("Server not found") + callAuthenticate() + } + + private suspend fun callAuthenticate(): String = + subject.authenticate(TEST_USERNAME, TEST_PASSWORD, TEST_BASE_URL) + companion object { - private const val TEST_BASE_URL = "" + const val TEST_USERNAME = "TEST_USERNAME" + const val TEST_PASSWORD = "TEST_PASSWORD" + const val TEST_BASE_URL = "https://example.com/" + const val TEST_TOKEN = "TEST_TOKEN" + const val TEST_AUTH_HEADER = "Bearer TEST_TOKEN" } } \ No newline at end of file From df6d2f68aa994cd856d40b0fae60f2c6e04db423 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 7 Aug 2022 12:42:23 +0200 Subject: [PATCH 09/13] Fix imports in tests --- .../network/RetrofitServiceFactoryTest.kt | 72 ------------------- .../recipes/impl/RecipesRemoteMediatorTest.kt | 2 +- .../mealient/test/RecipeImplTestData.kt | 8 +-- .../mealient/ui/add/AddRecipeViewModelTest.kt | 2 +- 4 files changed, 6 insertions(+), 78 deletions(-) delete mode 100644 app/src/test/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactoryTest.kt diff --git a/app/src/test/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactoryTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactoryTest.kt deleted file mode 100644 index 59050c6..0000000 --- a/app/src/test/java/gq/kirmanak/mealient/data/network/RetrofitServiceFactoryTest.kt +++ /dev/null @@ -1,72 +0,0 @@ -package gq.kirmanak.mealient.data.network - -import com.google.common.truth.Truth.assertThat -import gq.kirmanak.mealient.data.baseurl.BaseURLStorage -import gq.kirmanak.mealient.data.baseurl.impl.VersionService -import gq.kirmanak.mealient.logging.Logger -import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL -import io.mockk.* -import io.mockk.impl.annotations.MockK -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.runTest -import org.junit.Before -import org.junit.Test -import retrofit2.Retrofit - -@OptIn(ExperimentalCoroutinesApi::class) -class RetrofitServiceFactoryTest { - - @MockK - lateinit var retrofitBuilder: RetrofitBuilder - - @MockK - lateinit var baseURLStorage: BaseURLStorage - - @MockK - lateinit var retrofit: Retrofit - - @MockK - lateinit var versionService: VersionService - - @MockK(relaxUnitFun = true) - lateinit var logger: Logger - - lateinit var subject: ServiceFactory - - @Before - fun setUp() { - MockKAnnotations.init(this) - subject = retrofitBuilder.createServiceFactory(baseURLStorage, logger) - coEvery { retrofitBuilder.buildRetrofit(any()) } returns retrofit - every { retrofit.create(eq(VersionService::class.java)) } returns versionService - coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL - } - - @Test - fun `when provideService and url is null then url storage requested`() = runTest { - subject.provideService() - coVerify { baseURLStorage.requireBaseURL() } - } - - @Test - fun `when provideService and url is null then service still provided`() = runTest { - assertThat(subject.provideService()).isEqualTo(versionService) - } - - @Test - fun `when provideService called twice then builder called once`() = runTest { - subject.provideService() - subject.provideService() - coVerifyAll { retrofitBuilder.buildRetrofit(eq(TEST_BASE_URL)) } - } - - @Test - fun `when provideService called secondly with new url then builder called twice`() = runTest { - subject.provideService() - subject.provideService("new url") - coVerifyAll { - retrofitBuilder.buildRetrofit(eq(TEST_BASE_URL)) - retrofitBuilder.buildRetrofit(eq("new url")) - } - } -} \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediatorTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediatorTest.kt index 6e25216..62f4375 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediatorTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediatorTest.kt @@ -3,10 +3,10 @@ package gq.kirmanak.mealient.data.recipes.impl import androidx.paging.* import androidx.paging.LoadType.* import com.google.common.truth.Truth.assertThat -import gq.kirmanak.mealient.data.network.NetworkError.Unauthorized import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity +import gq.kirmanak.mealient.datasource.models.NetworkError.Unauthorized import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.test.RecipeImplTestData.TEST_RECIPE_SUMMARIES import io.mockk.MockKAnnotations diff --git a/app/src/test/java/gq/kirmanak/mealient/test/RecipeImplTestData.kt b/app/src/test/java/gq/kirmanak/mealient/test/RecipeImplTestData.kt index 2b9578c..d64ec91 100644 --- a/app/src/test/java/gq/kirmanak/mealient/test/RecipeImplTestData.kt +++ b/app/src/test/java/gq/kirmanak/mealient/test/RecipeImplTestData.kt @@ -1,10 +1,10 @@ package gq.kirmanak.mealient.test -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.* +import gq.kirmanak.mealient.datasource.models.GetRecipeIngredientResponse +import gq.kirmanak.mealient.datasource.models.GetRecipeInstructionResponse +import gq.kirmanak.mealient.datasource.models.GetRecipeResponse +import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse import kotlinx.datetime.LocalDate import kotlinx.datetime.LocalDateTime diff --git a/app/src/test/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModelTest.kt b/app/src/test/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModelTest.kt index 1f289bc..42afb57 100644 --- a/app/src/test/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModelTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/ui/add/AddRecipeViewModelTest.kt @@ -2,7 +2,7 @@ package gq.kirmanak.mealient.ui.add import com.google.common.truth.Truth.assertThat 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.logging.Logger import io.mockk.MockKAnnotations import io.mockk.coEvery From 8ee77b1ab8953f42057f8d583f7112fba4c876a0 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 7 Aug 2022 12:42:40 +0200 Subject: [PATCH 10/13] Fix AuthRepoImplTest --- .../data/auth/impl/AuthRepoImplTest.kt | 29 +++++++++++++++---- 1 file changed, 23 insertions(+), 6 deletions(-) diff --git a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt index 64f828b..e180433 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/auth/impl/AuthRepoImplTest.kt @@ -4,8 +4,10 @@ import com.google.common.truth.Truth.assertThat 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.logging.Logger import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER +import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN import gq.kirmanak.mealient.test.AuthImplTestData.TEST_USERNAME @@ -24,6 +26,9 @@ class AuthRepoImplTest { @MockK lateinit var dataSource: AuthDataSource + @MockK + lateinit var baseURLStorage: BaseURLStorage + @MockK(relaxUnitFun = true) lateinit var storage: AuthStorage @@ -35,7 +40,7 @@ class AuthRepoImplTest { @Before fun setUp() { MockKAnnotations.init(this) - subject = AuthRepoImpl(storage, dataSource, logger) + subject = AuthRepoImpl(storage, dataSource, baseURLStorage, logger) } @Test @@ -46,7 +51,14 @@ class AuthRepoImplTest { @Test fun `when authenticate successfully then saves to storage`() = runTest { - coEvery { dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD)) } returns TEST_TOKEN + coEvery { + dataSource.authenticate( + eq(TEST_USERNAME), + eq(TEST_PASSWORD), + eq(TEST_BASE_URL) + ) + } returns TEST_TOKEN + coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL subject.authenticate(TEST_USERNAME, TEST_PASSWORD) coVerifyAll { storage.setAuthHeader(TEST_AUTH_HEADER) @@ -58,7 +70,8 @@ class AuthRepoImplTest { @Test fun `when authenticate fails then does not change storage`() = runTest { - coEvery { dataSource.authenticate(any(), any()) } throws RuntimeException() + coEvery { dataSource.authenticate(any(), any(), any()) } throws RuntimeException() + coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL runCatching { subject.authenticate("invalid", "") } confirmVerified(storage) } @@ -94,10 +107,13 @@ class AuthRepoImplTest { fun `when invalidate with credentials then calls authenticate`() = runTest { coEvery { storage.getEmail() } returns TEST_USERNAME coEvery { storage.getPassword() } returns TEST_PASSWORD - coEvery { dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD)) } returns TEST_TOKEN + coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL + coEvery { + dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD), eq(TEST_BASE_URL)) + } returns TEST_TOKEN subject.invalidateAuthHeader() coVerifyAll { - dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD)) + dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD), eq(TEST_BASE_URL)) } } @@ -105,7 +121,8 @@ class AuthRepoImplTest { fun `when invalidate with credentials and auth fails then clears email`() = runTest { coEvery { storage.getEmail() } returns "invalid" coEvery { storage.getPassword() } returns "" - coEvery { dataSource.authenticate(any(), any()) } throws RuntimeException() + coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL + coEvery { dataSource.authenticate(any(), any(), any()) } throws RuntimeException() subject.invalidateAuthHeader() coVerify { storage.setEmail(null) } } From 9d8d07fb490f9c8ecf3ef13e76ed2df7a60763f8 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 7 Aug 2022 12:43:21 +0200 Subject: [PATCH 11/13] Remove AuthenticationInterceptorTest --- .../network/AuthenticationInterceptorTest.kt | 121 ------------------ 1 file changed, 121 deletions(-) delete mode 100644 app/src/test/java/gq/kirmanak/mealient/data/network/AuthenticationInterceptorTest.kt diff --git a/app/src/test/java/gq/kirmanak/mealient/data/network/AuthenticationInterceptorTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/network/AuthenticationInterceptorTest.kt deleted file mode 100644 index e8419dd..0000000 --- a/app/src/test/java/gq/kirmanak/mealient/data/network/AuthenticationInterceptorTest.kt +++ /dev/null @@ -1,121 +0,0 @@ -package gq.kirmanak.mealient.data.network - -import com.google.common.truth.Truth.assertThat -import gq.kirmanak.mealient.data.auth.AuthRepo -import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER -import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL -import io.mockk.* -import io.mockk.impl.annotations.MockK -import okhttp3.Interceptor -import okhttp3.Protocol -import okhttp3.Request -import okhttp3.Response -import org.junit.Before -import org.junit.Test - -class AuthenticationInterceptorTest { - @MockK(relaxUnitFun = true) - lateinit var authRepo: AuthRepo - - @MockK - lateinit var chain: Interceptor.Chain - - lateinit var subject: AuthenticationInterceptor - - @Before - fun setUp() { - MockKAnnotations.init(this) - subject = AuthenticationInterceptor(authRepo) - } - - @Test - fun `when intercept without header then response without header`() { - val request = createRequest() - val response = createResponse(request) - every { chain.request() } returns request - every { chain.proceed(any()) } returns response - coEvery { authRepo.getAuthHeader() } returns null - assertThat(subject.intercept(chain)).isEqualTo(response) - } - - @Test - fun `when intercept with header then chain called with header`() { - val request = createRequest() - val response = createResponse(request) - val requestSlot = slot() - - every { chain.request() } returns request - every { chain.proceed(capture(requestSlot)) } returns response - coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER - - subject.intercept(chain) - - assertThat(requestSlot.captured.header("Authorization")).isEqualTo(TEST_AUTH_HEADER) - } - - @Test - fun `when intercept with stale header then calls invalidate`() { - val request = createRequest() - val response = createResponse(request, code = 403) - - every { chain.request() } returns request - every { chain.proceed(any()) } returns response - coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER - - subject.intercept(chain) - - coVerifySequence { - authRepo.getAuthHeader() - authRepo.invalidateAuthHeader() - authRepo.getAuthHeader() - } - } - - @Test - fun `when intercept with proper header then requests auth header once`() { - val request = createRequest() - val response = createResponse(request) - - every { chain.request() } returns request - every { chain.proceed(any()) } returns response - coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER - - subject.intercept(chain) - - coVerifySequence { authRepo.getAuthHeader() } - } - - - @Test - fun `when intercept with stale header then updates header`() { - val request = createRequest() - val response = createResponse(request, code = 403) - val requests = mutableListOf() - - every { chain.request() } returns request - every { chain.proceed(capture(requests)) } returns response - coEvery { authRepo.getAuthHeader() } returns TEST_AUTH_HEADER andThen "Bearer NEW TOKEN" - - subject.intercept(chain) - - assertThat(requests.size).isEqualTo(2) - assertThat(requests[0].header("Authorization")).isEqualTo(TEST_AUTH_HEADER) - assertThat(requests[1].header("Authorization")).isEqualTo("Bearer NEW TOKEN") - } - - private fun createRequest( - url: String = TEST_BASE_URL, - ): Request = Request.Builder() - .url(url) - .build() - - private fun createResponse( - request: Request, - code: Int = 200, - ): Response = Response.Builder() - .protocol(Protocol.HTTP_2) - .code(code) - .request(request) - .message("Doesn't matter") - .build() -} \ No newline at end of file From 72a0a286ff7dcb8dc46708f19b32f990da45ef80 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 7 Aug 2022 12:47:09 +0200 Subject: [PATCH 12/13] Format MealieDataSourceImpl --- .../mealient/datasource/MealieDataSourceImpl.kt | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt index 527fef5..31cc9ad 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt @@ -54,12 +54,11 @@ class MealieDataSourceImpl @Inject constructor( override suspend fun requestRecipes( baseUrl: String, token: String?, start: Int, limit: Int - ): List = - makeCall( - block = { getRecipeSummary("$baseUrl/api/recipes/summary", token, start, limit) }, - logMethod = { "requestRecipes" }, - logParameters = { "baseUrl = $baseUrl, token = $token, start = $start, limit = $limit" } - ).getOrThrowUnauthorized() + ): List = 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 From 3e838aba858f4cf02515016837ce9a5ac8127b7f Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sun, 7 Aug 2022 13:16:28 +0200 Subject: [PATCH 13/13] Add MealieDataSourceWrapperTest --- .../network/MealieDataSourceWrapperTest.kt | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 app/src/test/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapperTest.kt diff --git a/app/src/test/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapperTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapperTest.kt new file mode 100644 index 0000000..743aa5a --- /dev/null +++ b/app/src/test/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapperTest.kt @@ -0,0 +1,58 @@ +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.NetworkError +import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER +import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL +import gq.kirmanak.mealient.test.RecipeImplTestData.GET_CAKE_RESPONSE +import io.mockk.MockKAnnotations +import io.mockk.coEvery +import io.mockk.coVerifyAll +import io.mockk.impl.annotations.MockK +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import java.io.IOException + +@OptIn(ExperimentalCoroutinesApi::class) +class MealieDataSourceWrapperTest { + + @MockK + lateinit var baseURLStorage: BaseURLStorage + + @MockK(relaxUnitFun = true) + lateinit var authRepo: AuthRepo + + @MockK + lateinit var mealieDataSource: MealieDataSource + + lateinit var subject: MealieDataSourceWrapper + + @Before + fun setUp() { + MockKAnnotations.init(this) + subject = MealieDataSourceWrapper(baseURLStorage, authRepo, mealieDataSource) + } + + @Test + fun `when withAuthHeader fails with Unauthorized then invalidates auth`() = runTest { + coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL + coEvery { authRepo.getAuthHeader() } returns null andThen TEST_AUTH_HEADER + coEvery { + mealieDataSource.requestRecipeInfo(eq(TEST_BASE_URL), isNull(), eq("cake")) + } throws NetworkError.Unauthorized(IOException()) + coEvery { + mealieDataSource.requestRecipeInfo(eq(TEST_BASE_URL), eq(TEST_AUTH_HEADER), eq("cake")) + } returns GET_CAKE_RESPONSE + subject.requestRecipeInfo("cake") + coVerifyAll { + authRepo.getAuthHeader() + authRepo.invalidateAuthHeader() + authRepo.getAuthHeader() + } + } + +} \ No newline at end of file