From e0a4442e7231cda050b79679b0da6b54a46d9599 Mon Sep 17 00:00:00 2001 From: Kirill Kamakin Date: Sat, 6 Aug 2022 18:20:23 +0200 Subject: [PATCH] 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")