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/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
deleted file mode 100644
index 46f7f8c..0000000
--- a/app/src/main/java/gq/kirmanak/mealient/data/add/impl/AddRecipeDataSourceImpl.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-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.extensions.logAndMapErrors
-import gq.kirmanak.mealient.logging.Logger
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class AddRecipeDataSourceImpl @Inject constructor(
- private val addRecipeServiceFactory: ServiceFactory,
- private val logger: Logger,
-) : 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) },
- 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/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..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
@@ -1,56 +1,15 @@
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.extensions.logAndMapErrors
-import gq.kirmanak.mealient.logging.Logger
-import kotlinx.serialization.json.Json
-import retrofit2.HttpException
-import retrofit2.Response
+import gq.kirmanak.mealient.datasource.MealieDataSource
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 {
- logger.v { "authenticate() called with: username = $username, password = $password" }
- val authService = authServiceFactory.provideService()
- val response = sendRequest(authService, username, password)
- val accessToken = parseToken(response)
- 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)
- }
- }
+ 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/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
deleted file mode 100644
index 62ce4cd..0000000
--- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/VersionDataSourceImpl.kt
+++ /dev/null
@@ -1,29 +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.ServiceFactory
-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 serviceFactory: ServiceFactory,
- private val logger: Logger,
-) : 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() },
- 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/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..4bb6a63
--- /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.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.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
+
+@Singleton
+class MealieDataSourceWrapper @Inject constructor(
+ private val baseURLStorage: BaseURLStorage,
+ private val authRepo: AuthRepo,
+ private val mealieDataSource: MealieDataSource,
+) : AddRecipeDataSource, RecipeDataSource, VersionDataSource {
+
+ override suspend fun addRecipe(recipe: AddRecipeRequest): String =
+ withAuthHeader { token -> addRecipe(getUrl(), token, recipe) }
+
+ override suspend fun getVersionInfo(baseUrl: String): VersionInfo =
+ mealieDataSource.getVersionInfo(baseUrl).toVersionInfo()
+
+ override suspend fun requestRecipes(start: Int, limit: Int): List =
+ withAuthHeader { token -> requestRecipes(getUrl(), token, start, limit) }
+
+ 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
+ }
+ }
+}
\ 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..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
@@ -1,10 +1,10 @@
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
+ suspend fun requestRecipes(start: Int, limit: Int): List
suspend fun requestRecipeInfo(slug: String): GetRecipeResponse
}
\ No newline at end of file
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 1e191f2..0000000
--- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSourceImpl.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-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.logging.Logger
-import javax.inject.Inject
-import javax.inject.Singleton
-
-@Singleton
-class RecipeDataSourceImpl @Inject constructor(
- private val recipeServiceFactory: ServiceFactory,
- private val logger: Logger,
-) : 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)
- 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)
- 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..97f9ce8 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.data.network.MealieDataSourceWrapper
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
@@ -49,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/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..30300c3 100644
--- a/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt
+++ b/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt
@@ -2,47 +2,21 @@ 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 gq.kirmanak.mealient.data.network.MealieDataSourceWrapper
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
+ fun bindVersionDataSource(mealieDataSourceWrapper: MealieDataSourceWrapper): VersionDataSource
@Binds
@Singleton
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..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,10 +9,7 @@ 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.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
@@ -20,15 +17,9 @@ 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.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
@@ -37,7 +28,7 @@ interface RecipeModule {
@Binds
@Singleton
- fun provideRecipeDataSource(recipeDataSourceImpl: RecipeDataSourceImpl): RecipeDataSource
+ fun provideRecipeDataSource(mealieDataSourceWrapper: MealieDataSourceWrapper): RecipeDataSource
@Binds
@Singleton
@@ -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
deleted file mode 100644
index 1327d57..0000000
--- a/app/src/main/java/gq/kirmanak/mealient/extensions/NetworkExtensions.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package gq.kirmanak.mealient.extensions
-
-import gq.kirmanak.mealient.data.network.NetworkError
-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,
- noinline logProvider: () -> String
-): T = runCatchingExceptCancel(block).getOrElse {
- e(it, messageSupplier = logProvider)
- throw it.mapToNetworkError()
-}
\ No newline at end of file
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..2cd87a3 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.toVersionInfo() = 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/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/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/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) }
}
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/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
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
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/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",
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
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..c8fd1ae
--- /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,
+ limit: Int,
+ ): 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..31cc9ad
--- /dev/null
+++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt
@@ -0,0 +1,92 @@
+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 okhttp3.ResponseBody
+import retrofit2.HttpException
+import java.net.ConnectException
+import java.net.SocketTimeoutException
+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 = 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 = 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 = makeCall(
+ block = { getVersion("$baseUrl/api/debug/version") },
+ logMethod = { "getVersionInfo" },
+ logParameters = { "baseUrl = $baseUrl" },
+ ).getOrElse {
+ throw when (it) {
+ is HttpException, is SerializationException -> NetworkError.NotMealie(it)
+ is SocketTimeoutException, is ConnectException -> NetworkError.NoServerConnection(it)
+ else -> NetworkError.MalformedUrl(it)
+ }
+ }
+
+ override suspend fun requestRecipes(
+ baseUrl: String, token: String?, start: Int, limit: Int
+ ): List = 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 = 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()}" } }
+ }
+
+ @OptIn(ExperimentalSerializationApi::class)
+ private inline fun ResponseBody.decode(): R = json.decodeFromStream(byteStream())
+}
+
+private fun Result.getOrThrowUnauthorized(): T = getOrElse {
+ throw if (it is HttpException && it.code() in listOf(401, 403)) {
+ NetworkError.Unauthorized(it)
+ } else {
+ it
+ }
+}
\ 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..9750cf9
--- /dev/null
+++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt
@@ -0,0 +1,42 @@
+package gq.kirmanak.mealient.datasource
+
+import gq.kirmanak.mealient.datasource.DataSourceModule.Companion.AUTHORIZATION_HEADER_NAME
+import gq.kirmanak.mealient.datasource.models.*
+import retrofit2.http.*
+
+interface MealieService {
+
+ @FormUrlEncoded
+ @POST
+ suspend fun getToken(
+ @Url url: String,
+ @Field("username") username: String,
+ @Field("password") password: String,
+ ): GetTokenResponse
+
+ @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/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..fa82b9b
--- /dev/null
+++ b/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImplTest.kt
@@ -0,0 +1,117 @@
+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
+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.io.IOException
+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)
+ }
+
+ @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 {
+ 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
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
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")