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 1d53c6f..dc5769b 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,7 +1,6 @@ package gq.kirmanak.mealient.data.add -import gq.kirmanak.mealient.datasource.models.AddRecipeRequest - interface AddRecipeDataSource { - suspend fun addRecipe(recipe: AddRecipeRequest): String + + suspend fun addRecipe(recipe: AddRecipeInfo): String } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeInfo.kt b/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeInfo.kt new file mode 100644 index 0000000..bb51ac6 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/add/AddRecipeInfo.kt @@ -0,0 +1,23 @@ +package gq.kirmanak.mealient.data.add + +data class AddRecipeInfo( + val name: String, + val description: String, + val recipeYield: String, + val recipeIngredient: List, + val recipeInstructions: List, + val settings: AddRecipeSettingsInfo, +) + +data class AddRecipeSettingsInfo( + val disableComments: Boolean, + val public: Boolean, +) + +data class AddRecipeIngredientInfo( + val note: String, +) + +data class AddRecipeInstructionInfo( + val text: String, +) 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 ec59af7..a0c7620 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,13 +1,12 @@ package gq.kirmanak.mealient.data.add -import gq.kirmanak.mealient.datasource.models.AddRecipeRequest import kotlinx.coroutines.flow.Flow interface AddRecipeRepo { - val addRecipeRequestFlow: Flow + val addRecipeRequestFlow: Flow - suspend fun preserve(recipe: AddRecipeRequest) + suspend fun preserve(recipe: AddRecipeInfo) suspend fun clear() 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 2d3c096..9043ca8 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 @@ -1,10 +1,10 @@ package gq.kirmanak.mealient.data.add.impl import gq.kirmanak.mealient.data.add.AddRecipeDataSource +import gq.kirmanak.mealient.data.add.AddRecipeInfo import gq.kirmanak.mealient.data.add.AddRecipeRepo -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.toAddRecipeInfo import gq.kirmanak.mealient.extensions.toDraft import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.flow.Flow @@ -20,10 +20,10 @@ class AddRecipeRepoImpl @Inject constructor( private val logger: Logger, ) : AddRecipeRepo { - override val addRecipeRequestFlow: Flow - get() = addRecipeStorage.updates.map { it.toAddRecipeRequest() } + override val addRecipeRequestFlow: Flow + get() = addRecipeStorage.updates.map { it.toAddRecipeInfo() } - override suspend fun preserve(recipe: AddRecipeRequest) { + override suspend fun preserve(recipe: AddRecipeInfo) { logger.v { "preserveRecipe() called with: recipe = $recipe" } addRecipeStorage.save(recipe.toDraft()) } 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 71ee822..576ccab 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, baseUrl: String): String + suspend fun authenticate(username: String, password: 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 7cadcc0..f42444b 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,15 +1,28 @@ package gq.kirmanak.mealient.data.auth.impl import gq.kirmanak.mealient.data.auth.AuthDataSource -import gq.kirmanak.mealient.datasource.MealieDataSource +import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo +import gq.kirmanak.mealient.data.baseurl.ServerVersion +import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0 +import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1 import javax.inject.Inject import javax.inject.Singleton @Singleton class AuthDataSourceImpl @Inject constructor( - private val mealieDataSource: MealieDataSource, + private val serverInfoRepo: ServerInfoRepo, + private val v0Source: MealieDataSourceV0, + private val v1Source: MealieDataSourceV1, ) : AuthDataSource { - override suspend fun authenticate(username: String, password: String, baseUrl: String): String = - mealieDataSource.authenticate(baseUrl, username, password) + override suspend fun authenticate( + username: String, + password: String, + ): String { + val baseUrl = serverInfoRepo.requireUrl() + return when (serverInfoRepo.getVersion()) { + ServerVersion.V0 -> v0Source.authenticate(baseUrl, username, password) + ServerVersion.V1 -> v1Source.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 2fc9b2c..e963a62 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,8 +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.datasource.runCatchingExceptCancel import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -15,7 +14,6 @@ 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 { @@ -24,9 +22,9 @@ 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, baseURLStorage.requireBaseURL()) - .let { AUTH_HEADER_FORMAT.format(it) } - .let { authStorage.setAuthHeader(it) } + val token = authDataSource.authenticate(email, password) + val header = AUTH_HEADER_FORMAT.format(token) + authStorage.setAuthHeader(header) authStorage.setEmail(email) authStorage.setPassword(password) } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/BaseURLStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/BaseURLStorage.kt deleted file mode 100644 index 7864ea9..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/BaseURLStorage.kt +++ /dev/null @@ -1,10 +0,0 @@ -package gq.kirmanak.mealient.data.baseurl - -interface BaseURLStorage { - - suspend fun getBaseURL(): String? - - suspend fun requireBaseURL(): String - - suspend fun storeBaseURL(baseURL: String) -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepo.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepo.kt new file mode 100644 index 0000000..d4d7f42 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepo.kt @@ -0,0 +1,13 @@ +package gq.kirmanak.mealient.data.baseurl + +interface ServerInfoRepo { + + suspend fun getUrl(): String? + + suspend fun requireUrl(): String + + suspend fun getVersion(): ServerVersion + + suspend fun storeBaseURL(baseURL: String, version: String) +} + diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoImpl.kt new file mode 100644 index 0000000..f46c19a --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoRepoImpl.kt @@ -0,0 +1,52 @@ +package gq.kirmanak.mealient.data.baseurl + +import gq.kirmanak.mealient.datasource.NetworkError +import gq.kirmanak.mealient.logging.Logger +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ServerInfoRepoImpl @Inject constructor( + private val serverInfoStorage: ServerInfoStorage, + private val versionDataSource: VersionDataSource, + private val logger: Logger, +) : ServerInfoRepo { + + override suspend fun getUrl(): String? { + val result = serverInfoStorage.getBaseURL() + logger.v { "getUrl() returned: $result" } + return result + } + + override suspend fun requireUrl(): String { + val result = checkNotNull(getUrl()) { "Server URL was null when it was required" } + logger.v { "requireUrl() returned: $result" } + return result + } + + override suspend fun getVersion(): ServerVersion { + var version = serverInfoStorage.getServerVersion() + val serverVersion = if (version == null) { + logger.d { "getVersion: version is null, requesting" } + version = versionDataSource.getVersionInfo(requireUrl()).version + val result = determineServerVersion(version) + serverInfoStorage.storeServerVersion(version) + result + } else { + determineServerVersion(version) + } + logger.v { "getVersion() returned: $serverVersion from $version" } + return serverVersion + } + + private fun determineServerVersion(version: String): ServerVersion = when { + version.startsWith("v0") -> ServerVersion.V0 + version.startsWith("v1") -> ServerVersion.V1 + else -> throw NetworkError.NotMealie(IllegalStateException("Server version is unknown: $version")) + } + + override suspend fun storeBaseURL(baseURL: String, version: String) { + logger.v { "storeBaseURL() called with: baseURL = $baseURL, version = $version" } + serverInfoStorage.storeBaseURL(baseURL, version) + } +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoStorage.kt new file mode 100644 index 0000000..5863b40 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerInfoStorage.kt @@ -0,0 +1,12 @@ +package gq.kirmanak.mealient.data.baseurl + +interface ServerInfoStorage { + + suspend fun getBaseURL(): String? + + suspend fun storeBaseURL(baseURL: String, version: String) + + suspend fun storeServerVersion(version: String) + + suspend fun getServerVersion(): String? +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerVersion.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerVersion.kt new file mode 100644 index 0000000..0f133fc --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/ServerVersion.kt @@ -0,0 +1,3 @@ +package gq.kirmanak.mealient.data.baseurl + +enum class ServerVersion { V0, V1 } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt new file mode 100644 index 0000000..7fd6be0 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionDataSourceImpl.kt @@ -0,0 +1,37 @@ +package gq.kirmanak.mealient.data.baseurl + +import gq.kirmanak.mealient.datasource.runCatchingExceptCancel +import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0 +import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1 +import gq.kirmanak.mealient.extensions.toVersionInfo +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.coroutineScope +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class VersionDataSourceImpl @Inject constructor( + private val v0Source: MealieDataSourceV0, + private val v1Source: MealieDataSourceV1, +) : VersionDataSource { + + override suspend fun getVersionInfo(baseUrl: String): VersionInfo { + val responses = coroutineScope { + val v0Deferred = async { + runCatchingExceptCancel { v0Source.getVersionInfo(baseUrl).toVersionInfo() } + } + val v1Deferred = async { + runCatchingExceptCancel { v1Source.getVersionInfo(baseUrl).toVersionInfo() } + } + listOf(v0Deferred, v1Deferred).awaitAll() + } + val firstSuccess = responses.firstNotNullOfOrNull { it.getOrNull() } + if (firstSuccess == null) { + throw responses.firstNotNullOf { it.exceptionOrNull() } + } else { + return firstSuccess + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionInfo.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionInfo.kt index 0e24c3b..a1eb010 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionInfo.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/VersionInfo.kt @@ -1,7 +1,5 @@ package gq.kirmanak.mealient.data.baseurl data class VersionInfo( - val production: Boolean, val version: String, - val demoStatus: Boolean, ) \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/BaseURLStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/BaseURLStorageImpl.kt deleted file mode 100644 index 64280cf..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/BaseURLStorageImpl.kt +++ /dev/null @@ -1,26 +0,0 @@ -package gq.kirmanak.mealient.data.baseurl.impl - -import androidx.datastore.preferences.core.Preferences -import gq.kirmanak.mealient.data.baseurl.BaseURLStorage -import gq.kirmanak.mealient.data.storage.PreferencesStorage -import javax.inject.Inject -import javax.inject.Singleton - -@Singleton -class BaseURLStorageImpl @Inject constructor( - private val preferencesStorage: PreferencesStorage, -) : BaseURLStorage { - - private val baseUrlKey: Preferences.Key - get() = preferencesStorage.baseUrlKey - - override suspend fun getBaseURL(): String? = preferencesStorage.getValue(baseUrlKey) - - override suspend fun requireBaseURL(): String = checkNotNull(getBaseURL()) { - "Base URL was null when it was required" - } - - override suspend fun storeBaseURL(baseURL: String) { - preferencesStorage.storeValues(Pair(baseUrlKey, baseURL)) - } -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/ServerInfoStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/ServerInfoStorageImpl.kt new file mode 100644 index 0000000..0f850f4 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/baseurl/impl/ServerInfoStorageImpl.kt @@ -0,0 +1,36 @@ +package gq.kirmanak.mealient.data.baseurl.impl + +import androidx.datastore.preferences.core.Preferences +import gq.kirmanak.mealient.data.baseurl.ServerInfoStorage +import gq.kirmanak.mealient.data.storage.PreferencesStorage +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class ServerInfoStorageImpl @Inject constructor( + private val preferencesStorage: PreferencesStorage, +) : ServerInfoStorage { + + private val baseUrlKey: Preferences.Key + get() = preferencesStorage.baseUrlKey + + private val serverVersionKey: Preferences.Key + get() = preferencesStorage.serverVersionKey + + override suspend fun getBaseURL(): String? = getValue(baseUrlKey) + + override suspend fun storeBaseURL(baseURL: String, version: String) { + preferencesStorage.storeValues( + Pair(baseUrlKey, baseURL), + Pair(serverVersionKey, version), + ) + } + + override suspend fun getServerVersion(): String? = getValue(serverVersionKey) + + override suspend fun storeServerVersion(version: String) { + preferencesStorage.storeValues(Pair(serverVersionKey, version)) + } + + private suspend fun getValue(key: Preferences.Key): T? = preferencesStorage.getValue(key) +} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt b/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt index 4bb6a63..1fbd9d3 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt @@ -1,49 +1,80 @@ package gq.kirmanak.mealient.data.network import gq.kirmanak.mealient.data.add.AddRecipeDataSource +import gq.kirmanak.mealient.data.add.AddRecipeInfo 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.baseurl.ServerInfoRepo +import gq.kirmanak.mealient.data.baseurl.ServerVersion +import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo 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 gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo +import gq.kirmanak.mealient.datasource.NetworkError +import gq.kirmanak.mealient.datasource.runCatchingExceptCancel +import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0 +import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1 +import gq.kirmanak.mealient.extensions.* import javax.inject.Inject import javax.inject.Singleton @Singleton class MealieDataSourceWrapper @Inject constructor( - private val baseURLStorage: BaseURLStorage, + private val serverInfoRepo: ServerInfoRepo, private val authRepo: AuthRepo, - private val mealieDataSource: MealieDataSource, -) : AddRecipeDataSource, RecipeDataSource, VersionDataSource { + private val v0Source: MealieDataSourceV0, + private val v1Source: MealieDataSourceV1, +) : AddRecipeDataSource, RecipeDataSource { - override suspend fun addRecipe(recipe: AddRecipeRequest): String = - withAuthHeader { token -> addRecipe(getUrl(), token, recipe) } + override suspend fun addRecipe( + recipe: AddRecipeInfo, + ): String = makeCall { token, url, version -> + when (version) { + ServerVersion.V0 -> v0Source.addRecipe(url, token, recipe.toV0Request()) + ServerVersion.V1 -> { + val slug = v1Source.createRecipe(url, token, recipe.toV1CreateRequest()) + v1Source.updateRecipe(url, token, slug, recipe.toV1UpdateRequest()) + slug + } + } + } - override suspend fun getVersionInfo(baseUrl: String): VersionInfo = - mealieDataSource.getVersionInfo(baseUrl).toVersionInfo() + override suspend fun requestRecipes( + start: Int, + limit: Int, + ): List = makeCall { token, url, version -> + when (version) { + ServerVersion.V0 -> { + v0Source.requestRecipes(url, token, start, limit).map { it.toRecipeSummaryInfo() } + } + ServerVersion.V1 -> { + // Imagine start is 30 and limit is 15. It means that we already have page 1 and 2, now we need page 3 + val page = start / limit + 1 + v1Source.requestRecipes(url, token, page, limit).map { it.toRecipeSummaryInfo() } + } + } + } - override suspend fun requestRecipes(start: Int, limit: Int): List = - withAuthHeader { token -> requestRecipes(getUrl(), token, start, limit) } + override suspend fun requestRecipeInfo( + slug: String, + ): FullRecipeInfo = makeCall { token, url, version -> + when (version) { + ServerVersion.V0 -> v0Source.requestRecipeInfo(url, token, slug).toFullRecipeInfo() + ServerVersion.V1 -> v1Source.requestRecipeInfo(url, token, slug).toFullRecipeInfo() + } + } - 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 { + private suspend inline fun makeCall(block: (String?, String, ServerVersion) -> T): T { + val authHeader = authRepo.getAuthHeader() + val url = serverInfoRepo.requireUrl() + val version = serverInfoRepo.getVersion() + return runCatchingExceptCancel { block(authHeader, url, version) }.getOrElse { if (it is NetworkError.Unauthorized) { authRepo.invalidateAuthHeader() // Trying again with new authentication header - mealieDataSource.block(authRepo.getAuthHeader()) + val newHeader = authRepo.getAuthHeader() + if (newHeader == authHeader) throw it else block(newHeader, url, version) } else { throw it } } + } } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt index 503a7b2..4cd3b1b 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/RecipeRepo.kt @@ -1,7 +1,7 @@ package gq.kirmanak.mealient.data.recipes import androidx.paging.Pager -import gq.kirmanak.mealient.database.recipe.entity.FullRecipeInfo +import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity interface RecipeRepo { @@ -9,5 +9,5 @@ interface RecipeRepo { suspend fun clearLocalData() - suspend fun loadRecipeInfo(recipeId: Long, recipeSlug: String): FullRecipeInfo + suspend fun loadRecipeInfo(recipeId: String, recipeSlug: String): FullRecipeEntity } \ 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 0ff12a5..431e74a 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,21 +1,21 @@ package gq.kirmanak.mealient.data.recipes.db import androidx.paging.PagingSource -import gq.kirmanak.mealient.database.recipe.entity.FullRecipeInfo +import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo +import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo +import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity 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) + suspend fun saveRecipes(recipes: List) fun queryRecipes(): PagingSource - suspend fun refreshAll(recipes: List) + suspend fun refreshAll(recipes: List) suspend fun clearAllLocalData() - suspend fun saveRecipeInfo(recipe: GetRecipeResponse) + suspend fun saveRecipeInfo(recipe: FullRecipeInfo) - suspend fun queryRecipeInfo(recipeId: Long): FullRecipeInfo + suspend fun queryRecipeInfo(recipeId: String): FullRecipeEntity } \ No newline at end of file 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 e5d777f..de086fc 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,12 @@ package gq.kirmanak.mealient.data.recipes.db import androidx.paging.PagingSource import androidx.room.withTransaction +import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo +import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo 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.database.recipe.entity.FullRecipeEntity +import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.extensions.recipeEntity import gq.kirmanak.mealient.extensions.toRecipeEntity import gq.kirmanak.mealient.extensions.toRecipeIngredientEntity @@ -23,71 +24,14 @@ class RecipeStorageImpl @Inject constructor( private val recipeDao: RecipeDao by lazy { db.recipeDao() } override suspend fun saveRecipes( - recipes: List + recipes: List ) = db.withTransaction { logger.v { "saveRecipes() called with $recipes" } - val tagEntities = mutableSetOf() - tagEntities.addAll(recipeDao.queryAllTags()) - - val categoryEntities = mutableSetOf() - categoryEntities.addAll(recipeDao.queryAllCategories()) - - val tagRecipeEntities = mutableSetOf() - val categoryRecipeEntities = mutableSetOf() - for (recipe in recipes) { val recipeSummaryEntity = recipe.recipeEntity() recipeDao.insertRecipe(recipeSummaryEntity) - - for (tag in recipe.tags) { - val tagId = getIdOrInsert(tagEntities, tag) - tagRecipeEntities += TagRecipeEntity(tagId, recipeSummaryEntity.remoteId) - } - - for (category in recipe.recipeCategories) { - val categoryId = getOrInsert(categoryEntities, category) - categoryRecipeEntities += CategoryRecipeEntity( - categoryId, - recipeSummaryEntity.remoteId - ) - } } - - recipeDao.insertTagRecipeEntities(tagRecipeEntities) - recipeDao.insertCategoryRecipeEntities(categoryRecipeEntities) - } - - private suspend fun getOrInsert( - categoryEntities: MutableSet, - category: String - ): Long { - val existingCategory = categoryEntities.find { it.name == category } - val categoryId = if (existingCategory == null) { - val categoryEntity = CategoryEntity(name = category) - val newId = recipeDao.insertCategory(categoryEntity) - categoryEntities.add(categoryEntity.copy(localId = newId)) - newId - } else { - existingCategory.localId - } - return categoryId - } - - private suspend fun getIdOrInsert( - tagEntities: MutableSet, - tag: String - ): Long { - val existingTag = tagEntities.find { it.name == tag } - val tagId = if (existingTag == null) { - val tagEntity = TagEntity(name = tag) - val newId = recipeDao.insertTag(tagEntity) - tagEntities.add(tagEntity.copy(localId = newId)) - newId - } else { - existingTag.localId - } - return tagId } @@ -96,7 +40,7 @@ class RecipeStorageImpl @Inject constructor( return recipeDao.queryRecipesByPages() } - override suspend fun refreshAll(recipes: List) { + override suspend fun refreshAll(recipes: List) { logger.v { "refreshAll() called with: recipes = $recipes" } db.withTransaction { recipeDao.removeAllRecipes() @@ -108,12 +52,10 @@ class RecipeStorageImpl @Inject constructor( logger.v { "clearAllLocalData() called" } db.withTransaction { recipeDao.removeAllRecipes() - recipeDao.removeAllCategories() - recipeDao.removeAllTags() } } - override suspend fun saveRecipeInfo(recipe: GetRecipeResponse) { + override suspend fun saveRecipeInfo(recipe: FullRecipeInfo) { logger.v { "saveRecipeInfo() called with: recipe = $recipe" } db.withTransaction { recipeDao.insertRecipe(recipe.toRecipeEntity()) @@ -132,7 +74,7 @@ class RecipeStorageImpl @Inject constructor( } } - override suspend fun queryRecipeInfo(recipeId: Long): FullRecipeInfo { + override suspend fun queryRecipeInfo(recipeId: String): FullRecipeEntity { logger.v { "queryRecipeInfo() called with: recipeId = $recipeId" } val fullRecipeInfo = checkNotNull(recipeDao.queryFullRecipeInfo(recipeId)) { "Can't find recipe by id $recipeId in DB" diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImpl.kt index d5b30fa..5c693ec 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImpl.kt @@ -1,6 +1,6 @@ package gq.kirmanak.mealient.data.recipes.impl -import gq.kirmanak.mealient.data.baseurl.BaseURLStorage +import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.logging.Logger import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import javax.inject.Inject @@ -8,7 +8,7 @@ import javax.inject.Singleton @Singleton class RecipeImageUrlProviderImpl @Inject constructor( - private val baseURLStorage: BaseURLStorage, + private val serverInfoRepo: ServerInfoRepo, private val logger: Logger, ) : RecipeImageUrlProvider { @@ -16,7 +16,7 @@ class RecipeImageUrlProviderImpl @Inject constructor( logger.v { "generateImageUrl() called with: slug = $slug" } slug?.takeUnless { it.isBlank() } ?: return null val imagePath = IMAGE_PATH_FORMAT.format(slug) - val baseUrl = baseURLStorage.getBaseURL()?.takeUnless { it.isEmpty() } + val baseUrl = serverInfoRepo.getUrl()?.takeUnless { it.isEmpty() } val result = baseUrl?.toHttpUrlOrNull() ?.newBuilder() ?.addPathSegments(imagePath) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt index 942fe5f..a48691f 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImpl.kt @@ -7,9 +7,9 @@ import androidx.paging.PagingConfig import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource -import gq.kirmanak.mealient.database.recipe.entity.FullRecipeInfo +import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity -import gq.kirmanak.mealient.extensions.runCatchingExceptCancel +import gq.kirmanak.mealient.datasource.runCatchingExceptCancel import gq.kirmanak.mealient.logging.Logger import javax.inject.Inject import javax.inject.Singleton @@ -38,7 +38,7 @@ class RecipeRepoImpl @Inject constructor( storage.clearAllLocalData() } - override suspend fun loadRecipeInfo(recipeId: Long, recipeSlug: String): FullRecipeInfo { + override suspend fun loadRecipeInfo(recipeId: String, recipeSlug: String): FullRecipeEntity { logger.v { "loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug" } runCatchingExceptCancel { diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt index 06e785a..435d76d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/impl/RecipesRemoteMediator.kt @@ -7,7 +7,7 @@ import androidx.paging.LoadType.REFRESH 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.extensions.runCatchingExceptCancel +import gq.kirmanak.mealient.datasource.runCatchingExceptCancel import gq.kirmanak.mealient.logging.Logger import javax.inject.Inject import javax.inject.Singleton diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/FullRecipeInfo.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/FullRecipeInfo.kt new file mode 100644 index 0000000..756e4ac --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/FullRecipeInfo.kt @@ -0,0 +1,17 @@ +package gq.kirmanak.mealient.data.recipes.network + +data class FullRecipeInfo( + val remoteId: String, + val name: String, + val recipeYield: String, + val recipeIngredients: List, + val recipeInstructions: List, +) + +data class RecipeIngredientInfo( + val note: String, +) + +data class RecipeInstructionInfo( + val text: String, +) 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 5e82886..4305eb5 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,7 @@ package gq.kirmanak.mealient.data.recipes.network -import gq.kirmanak.mealient.datasource.models.GetRecipeResponse -import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse - interface RecipeDataSource { - suspend fun requestRecipes(start: Int, limit: Int): List + suspend fun requestRecipes(start: Int, limit: Int): List - suspend fun requestRecipeInfo(slug: String): GetRecipeResponse + suspend fun requestRecipeInfo(slug: String): FullRecipeInfo } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeSummaryInfo.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeSummaryInfo.kt new file mode 100644 index 0000000..9e9cf8f --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeSummaryInfo.kt @@ -0,0 +1,14 @@ +package gq.kirmanak.mealient.data.recipes.network + +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime + +data class RecipeSummaryInfo( + val remoteId: String, + val name: String, + val slug: String, + val description: String = "", + val imageId: String, + val dateAdded: LocalDate, + val dateUpdated: LocalDateTime +) diff --git a/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorage.kt index 31e9174..df88285 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorage.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorage.kt @@ -7,6 +7,8 @@ interface PreferencesStorage { val baseUrlKey: Preferences.Key + val serverVersionKey: Preferences.Key + val isDisclaimerAcceptedKey: Preferences.Key suspend fun getValue(key: Preferences.Key): T? diff --git a/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorageImpl.kt index e42346c..f9e7bfa 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/storage/PreferencesStorageImpl.kt @@ -18,6 +18,8 @@ class PreferencesStorageImpl @Inject constructor( override val baseUrlKey = stringPreferencesKey("baseUrl") + override val serverVersionKey = stringPreferencesKey("serverVersion") + override val isDisclaimerAcceptedKey = booleanPreferencesKey("isDisclaimedAccepted") override suspend fun getValue(key: Preferences.Key): T? { 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 30300c3..2e6f1a6 100644 --- a/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt +++ b/app/src/main/java/gq/kirmanak/mealient/di/BaseURLModule.kt @@ -4,10 +4,8 @@ import dagger.Binds import dagger.Module 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.network.MealieDataSourceWrapper +import gq.kirmanak.mealient.data.baseurl.* +import gq.kirmanak.mealient.data.baseurl.impl.ServerInfoStorageImpl import javax.inject.Singleton @Module @@ -16,9 +14,13 @@ interface BaseURLModule { @Binds @Singleton - fun bindVersionDataSource(mealieDataSourceWrapper: MealieDataSourceWrapper): VersionDataSource + fun bindVersionDataSource(versionDataSourceImpl: VersionDataSourceImpl): VersionDataSource @Binds @Singleton - fun bindBaseUrlStorage(baseURLStorageImpl: BaseURLStorageImpl): BaseURLStorage + fun bindBaseUrlStorage(baseURLStorageImpl: ServerInfoStorageImpl): ServerInfoStorage + + @Binds + @Singleton + fun bindServerInfoRepo(serverInfoRepoImpl: ServerInfoRepoImpl): ServerInfoRepo } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/CoroutineExtensions.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/CoroutineExtensions.kt deleted file mode 100644 index c0b7379..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/CoroutineExtensions.kt +++ /dev/null @@ -1,15 +0,0 @@ -package gq.kirmanak.mealient.extensions - -import kotlinx.coroutines.CancellationException - -/** - * Like [runCatching] but rethrows [CancellationException] to support - * cancellation of coroutines. - */ -inline fun runCatchingExceptCancel(block: () -> T): Result = try { - Result.success(block()) -} catch (e: CancellationException) { - throw e -} catch (e: Throwable) { - Result.failure(e) -} \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/ModelMappings.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/ModelMappings.kt new file mode 100644 index 0000000..705ceb5 --- /dev/null +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/ModelMappings.kt @@ -0,0 +1,173 @@ +package gq.kirmanak.mealient.extensions + +import gq.kirmanak.mealient.data.add.AddRecipeInfo +import gq.kirmanak.mealient.data.add.AddRecipeIngredientInfo +import gq.kirmanak.mealient.data.add.AddRecipeInstructionInfo +import gq.kirmanak.mealient.data.add.AddRecipeSettingsInfo +import gq.kirmanak.mealient.data.baseurl.VersionInfo +import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo +import gq.kirmanak.mealient.data.recipes.network.RecipeIngredientInfo +import gq.kirmanak.mealient.data.recipes.network.RecipeInstructionInfo +import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo +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.v0.models.* +import gq.kirmanak.mealient.datasource.v1.models.* +import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft +import java.util.* + +fun FullRecipeInfo.toRecipeEntity() = RecipeEntity( + remoteId = remoteId, + recipeYield = recipeYield +) + +fun RecipeIngredientInfo.toRecipeIngredientEntity(remoteId: String) = RecipeIngredientEntity( + recipeId = remoteId, + note = note, +) + +fun RecipeInstructionInfo.toRecipeInstructionEntity(remoteId: String) = RecipeInstructionEntity( + recipeId = remoteId, + text = text +) + +fun GetRecipeSummaryResponseV0.toRecipeSummaryInfo() = RecipeSummaryInfo( + remoteId = remoteId.toString(), + name = name, + slug = slug, + description = description, + dateAdded = dateAdded, + dateUpdated = dateUpdated, + imageId = slug, +) + +fun GetRecipeSummaryResponseV1.toRecipeSummaryInfo() = RecipeSummaryInfo( + remoteId = remoteId, + name = name, + slug = slug, + description = description, + dateAdded = dateAdded, + dateUpdated = dateUpdated, + imageId = remoteId, +) + +fun RecipeSummaryInfo.recipeEntity() = RecipeSummaryEntity( + remoteId = remoteId, + name = name, + slug = slug, + description = description, + dateAdded = dateAdded, + dateUpdated = dateUpdated, + imageId = imageId, +) + +fun VersionResponseV0.toVersionInfo() = VersionInfo(version) + +fun VersionResponseV1.toVersionInfo() = VersionInfo(version) + +fun AddRecipeDraft.toAddRecipeInfo() = AddRecipeInfo( + name = recipeName, + description = recipeDescription, + recipeYield = recipeYield, + recipeIngredient = recipeIngredients.map { AddRecipeIngredientInfo(note = it) }, + recipeInstructions = recipeInstructions.map { AddRecipeInstructionInfo(text = it) }, + settings = AddRecipeSettingsInfo( + public = isRecipePublic, + disableComments = areCommentsDisabled, + ) +) + +fun AddRecipeInfo.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, +) + +fun GetRecipeResponseV0.toFullRecipeInfo() = FullRecipeInfo( + remoteId = remoteId.toString(), + name = name, + recipeYield = recipeYield, + recipeIngredients = recipeIngredients.map { it.toRecipeIngredientInfo() }, + recipeInstructions = recipeInstructions.map { it.toRecipeInstructionInfo() } +) + +fun GetRecipeIngredientResponseV0.toRecipeIngredientInfo() = RecipeIngredientInfo( + note = note, +) + +fun GetRecipeInstructionResponseV0.toRecipeInstructionInfo() = RecipeInstructionInfo( + text = text +) + +fun GetRecipeResponseV1.toFullRecipeInfo() = FullRecipeInfo( + remoteId = remoteId, + name = name, + recipeYield = recipeYield, + recipeIngredients = recipeIngredients.map { it.toRecipeIngredientInfo() }, + recipeInstructions = recipeInstructions.map { it.toRecipeInstructionInfo() } +) + +fun GetRecipeIngredientResponseV1.toRecipeIngredientInfo() = RecipeIngredientInfo( + note = note, +) + +fun GetRecipeInstructionResponseV1.toRecipeInstructionInfo() = RecipeInstructionInfo( + text = text +) + +fun AddRecipeInfo.toV0Request() = AddRecipeRequestV0( + name = name, + description = description, + recipeYield = recipeYield, + recipeIngredient = recipeIngredient.map { it.toV0Ingredient() }, + recipeInstructions = recipeInstructions.map { it.toV0Instruction() }, + settings = settings.toV0Settings(), +) + +private fun AddRecipeSettingsInfo.toV0Settings() = AddRecipeSettingsV0( + disableComments = disableComments, + public = public, +) + +private fun AddRecipeIngredientInfo.toV0Ingredient() = AddRecipeIngredientV0( + note = note, +) + +private fun AddRecipeInstructionInfo.toV0Instruction() = AddRecipeInstructionV0( + text = text, +) + + +fun AddRecipeInfo.toV1CreateRequest() = CreateRecipeRequestV1( + name = name, +) + +fun AddRecipeInfo.toV1UpdateRequest() = UpdateRecipeRequestV1( + description = description, + recipeYield = recipeYield, + recipeIngredient = recipeIngredient.map { it.toV1Ingredient() }, + recipeInstructions = recipeInstructions.map { it.toV1Instruction() }, + settings = settings.toV1Settings(), +) + +private fun AddRecipeSettingsInfo.toV1Settings() = AddRecipeSettingsV1( + disableComments = disableComments, + public = public, +) + +private fun AddRecipeIngredientInfo.toV1Ingredient() = AddRecipeIngredientV1( + id = UUID.randomUUID().toString(), + note = note, +) + +private fun AddRecipeInstructionInfo.toV1Instruction() = AddRecipeInstructionV1( + id = UUID.randomUUID().toString(), + text = text, + ingredientReferences = emptyList(), +) \ 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 deleted file mode 100644 index 2cd87a3..0000000 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappings.kt +++ /dev/null @@ -1,67 +0,0 @@ -package gq.kirmanak.mealient.extensions - -import gq.kirmanak.mealient.data.baseurl.VersionInfo -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, - recipeYield = recipeYield -) - -fun GetRecipeIngredientResponse.toRecipeIngredientEntity(remoteId: Long) = - RecipeIngredientEntity( - recipeId = remoteId, - title = title, - note = note, - unit = unit, - food = food, - disableAmount = disableAmount, - quantity = quantity - ) - -fun GetRecipeInstructionResponse.toRecipeInstructionEntity(remoteId: Long) = - RecipeInstructionEntity( - recipeId = remoteId, - title = title, - text = text - ) - -fun GetRecipeSummaryResponse.recipeEntity() = RecipeSummaryEntity( - remoteId = remoteId, - name = name, - slug = slug, - image = image, - description = description, - rating = rating, - dateAdded = dateAdded, - dateUpdated = dateUpdated, -) - -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 d8807b5..a746d50 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.AddRecipeInfo +import gq.kirmanak.mealient.data.add.AddRecipeIngredientInfo +import gq.kirmanak.mealient.data.add.AddRecipeInstructionInfo +import gq.kirmanak.mealient.data.add.AddRecipeSettingsInfo 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 @@ -122,14 +122,15 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) { private fun saveValues() = with(binding) { logger.v { "saveValues() called" } - val instructions = parseInputRows(instructionsFlow).map { AddRecipeInstruction(text = it) } - val ingredients = parseInputRows(ingredientsFlow).map { AddRecipeIngredient(note = it) } - val settings = AddRecipeSettings( + val instructions = + parseInputRows(instructionsFlow).map { AddRecipeInstructionInfo(text = it) } + val ingredients = parseInputRows(ingredientsFlow).map { AddRecipeIngredientInfo(note = it) } + val settings = AddRecipeSettingsInfo( public = publicRecipe.isChecked, disableComments = disableComments.isChecked, ) viewModel.preserve( - AddRecipeRequest( + AddRecipeInfo( name = recipeNameInput.text.toString(), description = recipeDescriptionInput.text.toString(), recipeYield = recipeYieldInput.text.toString(), @@ -148,7 +149,7 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) { .filterNot { it.isBlank() } .toList() - private fun onSavedInputLoaded(request: AddRecipeRequest) = with(binding) { + private fun onSavedInputLoaded(request: AddRecipeInfo) = with(binding) { logger.v { "onSavedInputLoaded() called with: request = $request" } recipeNameInput.setText(request.name) recipeDescriptionInput.setText(request.description) 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 b825eff..982e56f 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 @@ -3,9 +3,9 @@ package gq.kirmanak.mealient.ui.add import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import gq.kirmanak.mealient.data.add.AddRecipeInfo import gq.kirmanak.mealient.data.add.AddRecipeRepo -import gq.kirmanak.mealient.datasource.models.AddRecipeRequest -import gq.kirmanak.mealient.extensions.runCatchingExceptCancel +import gq.kirmanak.mealient.datasource.runCatchingExceptCancel import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.flow.Flow @@ -23,8 +23,8 @@ class AddRecipeViewModel @Inject constructor( private val _addRecipeResultChannel = Channel(Channel.UNLIMITED) val addRecipeResult: Flow get() = _addRecipeResultChannel.receiveAsFlow() - private val _preservedAddRecipeRequestChannel = Channel(Channel.UNLIMITED) - val preservedAddRecipeRequest: Flow + private val _preservedAddRecipeRequestChannel = Channel(Channel.UNLIMITED) + val preservedAddRecipeRequest: Flow get() = _preservedAddRecipeRequestChannel.receiveAsFlow() fun loadPreservedRequest() { @@ -47,7 +47,7 @@ class AddRecipeViewModel @Inject constructor( } } - fun preserve(request: AddRecipeRequest) { + fun preserve(request: AddRecipeInfo) { logger.v { "preserve() called with: request = $request" } viewModelScope.launch { addRecipeRepo.preserve(request) } } 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 651b16f..7a1dd16 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 @@ -10,7 +10,7 @@ import by.kirich1409.viewbindingdelegate.viewBinding import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.R import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding -import gq.kirmanak.mealient.datasource.models.NetworkError +import gq.kirmanak.mealient.datasource.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/auth/AuthenticationViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt index 55a3cdd..145852d 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/auth/AuthenticationViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.data.auth.AuthRepo -import gq.kirmanak.mealient.extensions.runCatchingExceptCancel +import gq.kirmanak.mealient.datasource.runCatchingExceptCancel import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.OperationUiState import kotlinx.coroutines.launch 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 288a2db..fb505dd 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 @@ -10,7 +10,7 @@ import by.kirich1409.viewbindingdelegate.viewBinding import dagger.hilt.android.AndroidEntryPoint import gq.kirmanak.mealient.R import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding -import gq.kirmanak.mealient.datasource.models.NetworkError +import gq.kirmanak.mealient.datasource.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/BaseURLViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt index 1b98c87..d2a66de 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModel.kt @@ -5,9 +5,9 @@ import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel -import gq.kirmanak.mealient.data.baseurl.BaseURLStorage +import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.baseurl.VersionDataSource -import gq.kirmanak.mealient.extensions.runCatchingExceptCancel +import gq.kirmanak.mealient.datasource.runCatchingExceptCancel import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.ui.OperationUiState import kotlinx.coroutines.launch @@ -15,7 +15,7 @@ import javax.inject.Inject @HiltViewModel class BaseURLViewModel @Inject constructor( - private val baseURLStorage: BaseURLStorage, + private val serverInfoRepo: ServerInfoRepo, private val versionDataSource: VersionDataSource, private val logger: Logger, ) : ViewModel() { @@ -35,8 +35,8 @@ class BaseURLViewModel @Inject constructor( logger.v { "checkBaseURL() called with: baseURL = $baseURL" } val result = runCatchingExceptCancel { // If it returns proper version info then it must be a Mealie - versionDataSource.getVersionInfo(baseURL) - baseURLStorage.storeBaseURL(baseURL) + val version = versionDataSource.getVersionInfo(baseURL).version + serverInfoRepo.storeBaseURL(baseURL, version) } logger.i { "checkBaseURL: result is $result" } _uiState.value = OperationUiState.fromResult(result) 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 45a7e4c..a1651b3 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 @@ -44,7 +44,7 @@ class RecipeModelLoader private constructor( options: Options? ): String? { logger.v { "getUrl() called with: model = $model, width = $width, height = $height, options = $options" } - return runBlocking { recipeImageUrlProvider.generateImageUrl(model?.slug) } + return runBlocking { recipeImageUrlProvider.generateImageUrl(model?.imageId) } } override fun getHeaders( diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoUiState.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoUiState.kt index 4d1fff5..6083fae 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoUiState.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoUiState.kt @@ -1,9 +1,9 @@ package gq.kirmanak.mealient.ui.recipes.info -import gq.kirmanak.mealient.database.recipe.entity.FullRecipeInfo +import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity data class RecipeInfoUiState( val areIngredientsVisible: Boolean = false, val areInstructionsVisible: Boolean = false, - val recipeInfo: FullRecipeInfo? = null, + val recipeInfo: FullRecipeEntity? = null, ) diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt index cf40aba..932ae33 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/recipes/info/RecipeInfoViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import gq.kirmanak.mealient.data.recipes.RecipeRepo -import gq.kirmanak.mealient.extensions.runCatchingExceptCancel +import gq.kirmanak.mealient.datasource.runCatchingExceptCancel import gq.kirmanak.mealient.logging.Logger import kotlinx.coroutines.launch import javax.inject.Inject @@ -20,7 +20,7 @@ class RecipeInfoViewModel @Inject constructor( private val _uiState = MutableLiveData(RecipeInfoUiState()) val uiState: LiveData get() = _uiState - fun loadRecipeInfo(recipeId: Long, recipeSlug: String) { + fun loadRecipeInfo(recipeId: String, recipeSlug: String) { logger.v { "loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug" } _uiState.value = RecipeInfoUiState() viewModelScope.launch { diff --git a/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashViewModel.kt b/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashViewModel.kt index 8fe4eb4..399f860 100644 --- a/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashViewModel.kt +++ b/app/src/main/java/gq/kirmanak/mealient/ui/splash/SplashViewModel.kt @@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import androidx.navigation.NavDirections import dagger.hilt.android.lifecycle.HiltViewModel -import gq.kirmanak.mealient.data.baseurl.BaseURLStorage +import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage import kotlinx.coroutines.delay import kotlinx.coroutines.launch @@ -15,7 +15,7 @@ import javax.inject.Inject @HiltViewModel class SplashViewModel @Inject constructor( private val disclaimerStorage: DisclaimerStorage, - private val baseURLStorage: BaseURLStorage, + private val serverInfoRepo: ServerInfoRepo, ) : ViewModel() { private val _nextDestination = MutableLiveData() val nextDestination: LiveData = _nextDestination @@ -25,7 +25,7 @@ class SplashViewModel @Inject constructor( delay(1000) _nextDestination.value = when { !disclaimerStorage.isDisclaimerAccepted() -> SplashFragmentDirections.actionSplashFragmentToDisclaimerFragment() - baseURLStorage.getBaseURL() == null -> SplashFragmentDirections.actionSplashFragmentToBaseURLFragment() + serverInfoRepo.getUrl() == null -> SplashFragmentDirections.actionSplashFragmentToBaseURLFragment() else -> SplashFragmentDirections.actionSplashFragmentToRecipesFragment() } } diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index 47fa15b..01445d5 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -36,7 +36,7 @@ app:argType="string" /> + app:argType="string" /> (relaxed = true) coEvery { - mealieDataSource.requestRecipeInfo(eq(TEST_BASE_URL), eq(TEST_AUTH_HEADER), eq("cake")) - } returns GET_CAKE_RESPONSE + v0Source.requestRecipeInfo(eq(TEST_BASE_URL), eq(TEST_AUTH_HEADER), eq("cake")) + } returns successResponse subject.requestRecipeInfo("cake") coVerifyAll { authRepo.getAuthHeader() diff --git a/app/src/test/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImplTest.kt index f3c18ab..8dddb8f 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImplTest.kt @@ -3,10 +3,6 @@ package gq.kirmanak.mealient.data.recipes.db import com.google.common.truth.Truth.assertThat import dagger.hilt.android.testing.HiltAndroidTest import gq.kirmanak.mealient.database.AppDb -import gq.kirmanak.mealient.database.recipe.entity.CategoryEntity -import gq.kirmanak.mealient.database.recipe.entity.CategoryRecipeEntity -import gq.kirmanak.mealient.database.recipe.entity.TagEntity -import gq.kirmanak.mealient.database.recipe.entity.TagRecipeEntity import gq.kirmanak.mealient.test.HiltRobolectricTest import gq.kirmanak.mealient.test.RecipeImplTestData.BREAD_INGREDIENT import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_BREAD_RECIPE_INGREDIENT_ENTITY @@ -36,28 +32,6 @@ class RecipeStorageImplTest : HiltRobolectricTest() { @Inject lateinit var appDb: AppDb - @Test - fun `when saveRecipes then saves tags`() = runTest { - subject.saveRecipes(TEST_RECIPE_SUMMARIES) - val actualTags = appDb.recipeDao().queryAllTags() - assertThat(actualTags).containsExactly( - TagEntity(localId = 1, name = "gluten"), - TagEntity(localId = 2, name = "allergic"), - TagEntity(localId = 3, name = "milk") - ) - } - - @Test - fun `when saveRecipes then saves categories`() = runTest { - subject.saveRecipes(TEST_RECIPE_SUMMARIES) - val actual = appDb.recipeDao().queryAllCategories() - assertThat(actual).containsExactly( - CategoryEntity(localId = 1, name = "dessert"), - CategoryEntity(localId = 2, name = "tasty"), - CategoryEntity(localId = 3, name = "porridge") - ) - } - @Test fun `when saveRecipes then saves recipes`() = runTest { subject.saveRecipes(TEST_RECIPE_SUMMARIES) @@ -68,30 +42,6 @@ class RecipeStorageImplTest : HiltRobolectricTest() { ) } - @Test - fun `when saveRecipes then saves category recipes`() = runTest { - subject.saveRecipes(TEST_RECIPE_SUMMARIES) - val actual = appDb.recipeDao().queryAllCategoryRecipes() - assertThat(actual).containsExactly( - CategoryRecipeEntity(categoryId = 1, recipeId = 1), - CategoryRecipeEntity(categoryId = 2, recipeId = 1), - CategoryRecipeEntity(categoryId = 3, recipeId = 2), - CategoryRecipeEntity(categoryId = 2, recipeId = 2) - ) - } - - @Test - fun `when saveRecipes then saves tag recipes`() = runTest { - subject.saveRecipes(TEST_RECIPE_SUMMARIES) - val actual = appDb.recipeDao().queryAllTagRecipes() - assertThat(actual).containsExactly( - TagRecipeEntity(tagId = 1, recipeId = 1), - TagRecipeEntity(tagId = 2, recipeId = 1), - TagRecipeEntity(tagId = 3, recipeId = 2), - TagRecipeEntity(tagId = 1, recipeId = 2), - ) - } - @Test fun `when refreshAll then old recipes aren't preserved`() = runTest { subject.saveRecipes(TEST_RECIPE_SUMMARIES) @@ -100,28 +50,6 @@ class RecipeStorageImplTest : HiltRobolectricTest() { assertThat(actual).containsExactly(CAKE_RECIPE_SUMMARY_ENTITY) } - @Test - fun `when refreshAll then old category recipes aren't preserved`() = runTest { - subject.saveRecipes(TEST_RECIPE_SUMMARIES) - subject.refreshAll(listOf(RECIPE_SUMMARY_CAKE)) - val actual = appDb.recipeDao().queryAllCategoryRecipes() - assertThat(actual).containsExactly( - CategoryRecipeEntity(categoryId = 1, recipeId = 1), - CategoryRecipeEntity(categoryId = 2, recipeId = 1), - ) - } - - @Test - fun `when refreshAll then old tag recipes aren't preserved`() = runTest { - subject.saveRecipes(TEST_RECIPE_SUMMARIES) - subject.refreshAll(listOf(RECIPE_SUMMARY_CAKE)) - val actual = appDb.recipeDao().queryAllTagRecipes() - assertThat(actual).containsExactly( - TagRecipeEntity(tagId = 1, recipeId = 1), - TagRecipeEntity(tagId = 2, recipeId = 1), - ) - } - @Test fun `when clearAllLocalData then recipes aren't preserved`() = runTest { subject.saveRecipes(TEST_RECIPE_SUMMARIES) @@ -130,27 +58,11 @@ class RecipeStorageImplTest : HiltRobolectricTest() { assertThat(actual).isEmpty() } - @Test - fun `when clearAllLocalData then categories aren't preserved`() = runTest { - subject.saveRecipes(TEST_RECIPE_SUMMARIES) - subject.clearAllLocalData() - val actual = appDb.recipeDao().queryAllCategories() - assertThat(actual).isEmpty() - } - - @Test - fun `when clearAllLocalData then tags aren't preserved`() = runTest { - subject.saveRecipes(TEST_RECIPE_SUMMARIES) - subject.clearAllLocalData() - val actual = appDb.recipeDao().queryAllTags() - assertThat(actual).isEmpty() - } - @Test fun `when saveRecipeInfo then saves recipe info`() = runTest { subject.saveRecipes(listOf(RECIPE_SUMMARY_CAKE)) subject.saveRecipeInfo(GET_CAKE_RESPONSE) - val actual = appDb.recipeDao().queryFullRecipeInfo(1) + val actual = appDb.recipeDao().queryFullRecipeInfo("1") assertThat(actual).isEqualTo(FULL_CAKE_INFO_ENTITY) } @@ -159,7 +71,7 @@ class RecipeStorageImplTest : HiltRobolectricTest() { subject.saveRecipes(listOf(RECIPE_SUMMARY_CAKE, RECIPE_SUMMARY_PORRIDGE)) subject.saveRecipeInfo(GET_CAKE_RESPONSE) subject.saveRecipeInfo(GET_PORRIDGE_RESPONSE) - val actual = appDb.recipeDao().queryFullRecipeInfo(2) + val actual = appDb.recipeDao().queryFullRecipeInfo("2") assertThat(actual).isEqualTo(FULL_PORRIDGE_INFO_ENTITY) } @@ -169,7 +81,7 @@ class RecipeStorageImplTest : HiltRobolectricTest() { subject.saveRecipeInfo(GET_CAKE_RESPONSE) val newRecipe = GET_CAKE_RESPONSE.copy(recipeIngredients = listOf(BREAD_INGREDIENT)) subject.saveRecipeInfo(newRecipe) - val actual = appDb.recipeDao().queryFullRecipeInfo(1)?.recipeIngredients + val actual = appDb.recipeDao().queryFullRecipeInfo("1")?.recipeIngredients val expected = listOf(CAKE_BREAD_RECIPE_INGREDIENT_ENTITY.copy(localId = 3)) assertThat(actual).isEqualTo(expected) } @@ -180,7 +92,7 @@ class RecipeStorageImplTest : HiltRobolectricTest() { subject.saveRecipeInfo(GET_CAKE_RESPONSE) val newRecipe = GET_CAKE_RESPONSE.copy(recipeInstructions = listOf(MIX_INSTRUCTION)) subject.saveRecipeInfo(newRecipe) - val actual = appDb.recipeDao().queryFullRecipeInfo(1)?.recipeInstructions + val actual = appDb.recipeDao().queryFullRecipeInfo("1")?.recipeInstructions val expected = listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY.copy(localId = 3)) assertThat(actual).isEqualTo(expected) } diff --git a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImplTest.kt index 230f70b..728bb60 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeImageUrlProviderImplTest.kt @@ -1,7 +1,7 @@ package gq.kirmanak.mealient.data.recipes.impl import com.google.common.truth.Truth.assertThat -import gq.kirmanak.mealient.data.baseurl.BaseURLStorage +import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.logging.Logger import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -17,7 +17,7 @@ class RecipeImageUrlProviderImplTest { lateinit var subject: RecipeImageUrlProvider @MockK - lateinit var baseURLStorage: BaseURLStorage + lateinit var serverInfoRepo: ServerInfoRepo @MockK(relaxUnitFun = true) lateinit var logger: Logger @@ -25,7 +25,7 @@ class RecipeImageUrlProviderImplTest { @Before fun setUp() { MockKAnnotations.init(this) - subject = RecipeImageUrlProviderImpl(baseURLStorage, logger) + subject = RecipeImageUrlProviderImpl(serverInfoRepo, logger) prepareBaseURL("https://google.com/") } @@ -81,6 +81,6 @@ class RecipeImageUrlProviderImplTest { } private fun prepareBaseURL(baseURL: String?) { - coEvery { baseURLStorage.getBaseURL() } returns baseURL + coEvery { serverInfoRepo.getUrl() } returns baseURL } } \ No newline at end of file diff --git a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImplTest.kt b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImplTest.kt index 2a8801f..b66b447 100644 --- a/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImplTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/data/recipes/impl/RecipeRepoImplTest.kt @@ -47,24 +47,24 @@ class RecipeRepoImplTest { @Test fun `when loadRecipeInfo then loads recipe`() = runTest { coEvery { dataSource.requestRecipeInfo(eq("cake")) } returns GET_CAKE_RESPONSE - coEvery { storage.queryRecipeInfo(eq(1)) } returns FULL_CAKE_INFO_ENTITY - val actual = subject.loadRecipeInfo(1, "cake") + coEvery { storage.queryRecipeInfo(eq("1")) } returns FULL_CAKE_INFO_ENTITY + val actual = subject.loadRecipeInfo("1", "cake") assertThat(actual).isEqualTo(FULL_CAKE_INFO_ENTITY) } @Test fun `when loadRecipeInfo then saves to DB`() = runTest { coEvery { dataSource.requestRecipeInfo(eq("cake")) } returns GET_CAKE_RESPONSE - coEvery { storage.queryRecipeInfo(eq(1)) } returns FULL_CAKE_INFO_ENTITY - subject.loadRecipeInfo(1, "cake") + coEvery { storage.queryRecipeInfo(eq("1")) } returns FULL_CAKE_INFO_ENTITY + subject.loadRecipeInfo("1", "cake") coVerify { storage.saveRecipeInfo(eq(GET_CAKE_RESPONSE)) } } @Test fun `when loadRecipeInfo with error then loads from DB`() = runTest { coEvery { dataSource.requestRecipeInfo(eq("cake")) } throws RuntimeException() - coEvery { storage.queryRecipeInfo(eq(1)) } returns FULL_CAKE_INFO_ENTITY - val actual = subject.loadRecipeInfo(1, "cake") + coEvery { storage.queryRecipeInfo(eq("1")) } returns FULL_CAKE_INFO_ENTITY + val actual = subject.loadRecipeInfo("1", "cake") assertThat(actual).isEqualTo(FULL_CAKE_INFO_ENTITY) } } \ 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 62f4375..f819e66 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 @@ -6,7 +6,7 @@ import com.google.common.truth.Truth.assertThat 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.datasource.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/extensions/RemoteToLocalMappingsTest.kt b/app/src/test/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappingsTest.kt index 56b8428..a43e6c9 100644 --- a/app/src/test/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappingsTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappingsTest.kt @@ -1,10 +1,10 @@ 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.data.add.AddRecipeInfo +import gq.kirmanak.mealient.data.add.AddRecipeIngredientInfo +import gq.kirmanak.mealient.data.add.AddRecipeInstructionInfo +import gq.kirmanak.mealient.data.add.AddRecipeSettingsInfo import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft import org.junit.Test @@ -22,42 +22,42 @@ class RemoteToLocalMappingsTest { areCommentsDisabled = true, ) - val expected = AddRecipeRequest( + val expected = AddRecipeInfo( name = "Recipe name", description = "Recipe description", recipeYield = "Recipe yield", recipeIngredient = listOf( - AddRecipeIngredient(note = "Recipe ingredient 1"), - AddRecipeIngredient(note = "Recipe ingredient 2") + AddRecipeIngredientInfo(note = "Recipe ingredient 1"), + AddRecipeIngredientInfo(note = "Recipe ingredient 2") ), recipeInstructions = listOf( - AddRecipeInstruction(text = "Recipe instruction 1"), - AddRecipeInstruction(text = "Recipe instruction 2") + AddRecipeInstructionInfo(text = "Recipe instruction 1"), + AddRecipeInstructionInfo(text = "Recipe instruction 2") ), - settings = AddRecipeSettings( + settings = AddRecipeSettingsInfo( public = false, disableComments = true, ) ) - assertThat(input.toAddRecipeRequest()).isEqualTo(expected) + assertThat(input.toAddRecipeInfo()).isEqualTo(expected) } @Test fun `when toDraft then fills fields correctly`() { - val request = AddRecipeRequest( + val request = AddRecipeInfo( name = "Recipe name", description = "Recipe description", recipeYield = "Recipe yield", recipeIngredient = listOf( - AddRecipeIngredient(note = "Recipe ingredient 1"), - AddRecipeIngredient(note = "Recipe ingredient 2") + AddRecipeIngredientInfo(note = "Recipe ingredient 1"), + AddRecipeIngredientInfo(note = "Recipe ingredient 2") ), recipeInstructions = listOf( - AddRecipeInstruction(text = "Recipe instruction 1"), - AddRecipeInstruction(text = "Recipe instruction 2") + AddRecipeInstructionInfo(text = "Recipe instruction 1"), + AddRecipeInstructionInfo(text = "Recipe instruction 2") ), - settings = AddRecipeSettings( + settings = AddRecipeSettingsInfo( public = false, disableComments = true, ) diff --git a/app/src/test/java/gq/kirmanak/mealient/test/AuthImplTestData.kt b/app/src/test/java/gq/kirmanak/mealient/test/AuthImplTestData.kt index feb4866..cc5c8fc 100644 --- a/app/src/test/java/gq/kirmanak/mealient/test/AuthImplTestData.kt +++ b/app/src/test/java/gq/kirmanak/mealient/test/AuthImplTestData.kt @@ -1,10 +1,13 @@ package gq.kirmanak.mealient.test +import gq.kirmanak.mealient.data.baseurl.ServerVersion + object AuthImplTestData { 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" - const val TEST_URL = "TEST_URL" + const val TEST_VERSION = "v0.5.6" + val TEST_SERVER_VERSION = ServerVersion.V0 } \ No newline at end of file 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 d64ec91..e9c5bb3 100644 --- a/app/src/test/java/gq/kirmanak/mealient/test/RecipeImplTestData.kt +++ b/app/src/test/java/gq/kirmanak/mealient/test/RecipeImplTestData.kt @@ -1,133 +1,95 @@ package gq.kirmanak.mealient.test +import gq.kirmanak.mealient.data.add.AddRecipeInfo +import gq.kirmanak.mealient.data.add.AddRecipeIngredientInfo +import gq.kirmanak.mealient.data.add.AddRecipeInstructionInfo +import gq.kirmanak.mealient.data.add.AddRecipeSettingsInfo +import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo +import gq.kirmanak.mealient.data.recipes.network.RecipeIngredientInfo +import gq.kirmanak.mealient.data.recipes.network.RecipeInstructionInfo +import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo 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 object RecipeImplTestData { - val RECIPE_SUMMARY_CAKE = GetRecipeSummaryResponse( - remoteId = 1, + val RECIPE_SUMMARY_CAKE = RecipeSummaryInfo( + remoteId = "1", name = "Cake", slug = "cake", - image = "86", description = "A tasty cake", - recipeCategories = listOf("dessert", "tasty"), - tags = listOf("gluten", "allergic"), - rating = 4, dateAdded = LocalDate.parse("2021-11-13"), dateUpdated = LocalDateTime.parse("2021-11-13T15:30:13"), + imageId = "cake", ) - val RECIPE_SUMMARY_PORRIDGE = GetRecipeSummaryResponse( - remoteId = 2, + val RECIPE_SUMMARY_PORRIDGE = RecipeSummaryInfo( + remoteId = "2", name = "Porridge", slug = "porridge", - image = "89", description = "A tasty porridge", - recipeCategories = listOf("porridge", "tasty"), - tags = listOf("gluten", "milk"), - rating = 5, dateAdded = LocalDate.parse("2021-11-12"), dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"), + imageId = "porridge", ) val TEST_RECIPE_SUMMARIES = listOf(RECIPE_SUMMARY_CAKE, RECIPE_SUMMARY_PORRIDGE) val CAKE_RECIPE_SUMMARY_ENTITY = RecipeSummaryEntity( - remoteId = 1, + remoteId = "1", name = "Cake", slug = "cake", - image = "86", description = "A tasty cake", - rating = 4, dateAdded = LocalDate.parse("2021-11-13"), - dateUpdated = LocalDateTime.parse("2021-11-13T15:30:13") + dateUpdated = LocalDateTime.parse("2021-11-13T15:30:13"), + imageId = "cake", ) val PORRIDGE_RECIPE_SUMMARY_ENTITY = RecipeSummaryEntity( - remoteId = 2, + remoteId = "2", name = "Porridge", slug = "porridge", - image = "89", description = "A tasty porridge", - rating = 5, dateAdded = LocalDate.parse("2021-11-12"), dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"), + imageId = "porridge", ) - private val SUGAR_INGREDIENT = GetRecipeIngredientResponse( - title = "Sugar", + private val SUGAR_INGREDIENT = RecipeIngredientInfo( note = "2 oz of white sugar", - unit = "", - food = "", - disableAmount = true, - quantity = 1 ) - val BREAD_INGREDIENT = GetRecipeIngredientResponse( - title = "Bread", + val BREAD_INGREDIENT = RecipeIngredientInfo( note = "2 oz of white bread", - unit = "", - food = "", - disableAmount = false, - quantity = 2 ) - private val MILK_INGREDIENT = GetRecipeIngredientResponse( - title = "Milk", + private val MILK_INGREDIENT = RecipeIngredientInfo( note = "2 oz of white milk", - unit = "", - food = "", - disableAmount = true, - quantity = 3 ) - val MIX_INSTRUCTION = GetRecipeInstructionResponse( - title = "Mix", + val MIX_INSTRUCTION = RecipeInstructionInfo( text = "Mix the ingredients" ) - private val BAKE_INSTRUCTION = GetRecipeInstructionResponse( - title = "Bake", + private val BAKE_INSTRUCTION = RecipeInstructionInfo( text = "Bake the ingredients" ) - private val BOIL_INSTRUCTION = GetRecipeInstructionResponse( - title = "Boil", + private val BOIL_INSTRUCTION = RecipeInstructionInfo( text = "Boil the ingredients" ) - val GET_CAKE_RESPONSE = GetRecipeResponse( - remoteId = 1, + val GET_CAKE_RESPONSE = FullRecipeInfo( + remoteId = "1", name = "Cake", - slug = "cake", - image = "86", - description = "A tasty cake", - recipeCategories = listOf("dessert", "tasty"), - tags = listOf("gluten", "allergic"), - rating = 4, - dateAdded = LocalDate.parse("2021-11-13"), - dateUpdated = LocalDateTime.parse("2021-11-13T15:30:13"), recipeYield = "4 servings", recipeIngredients = listOf(SUGAR_INGREDIENT, BREAD_INGREDIENT), recipeInstructions = listOf(MIX_INSTRUCTION, BAKE_INSTRUCTION) ) - val GET_PORRIDGE_RESPONSE = GetRecipeResponse( - remoteId = 2, + val GET_PORRIDGE_RESPONSE = FullRecipeInfo( + remoteId = "2", name = "Porridge", - slug = "porridge", - image = "89", - description = "A tasty porridge", - recipeCategories = listOf("porridge", "tasty"), - tags = listOf("gluten", "milk"), - rating = 5, - dateAdded = LocalDate.parse("2021-11-12"), - dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"), recipeYield = "3 servings", recipeIngredients = listOf(SUGAR_INGREDIENT, MILK_INGREDIENT), recipeInstructions = listOf(MIX_INSTRUCTION, BOIL_INSTRUCTION) @@ -135,46 +97,34 @@ object RecipeImplTestData { val MIX_CAKE_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( localId = 1, - recipeId = 1, - title = "Mix", + recipeId = "1", text = "Mix the ingredients", ) private val BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( localId = 2, - recipeId = 1, - title = "Bake", + recipeId = "1", text = "Bake the ingredients", ) private val CAKE_RECIPE_ENTITY = RecipeEntity( - remoteId = 1, + remoteId = "1", recipeYield = "4 servings" ) private val CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( localId = 1, - recipeId = 1, - title = "Sugar", + recipeId = "1", note = "2 oz of white sugar", - unit = "", - food = "", - disableAmount = true, - quantity = 1 ) val CAKE_BREAD_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( localId = 2, - recipeId = 1, - title = "Bread", + recipeId = "1", note = "2 oz of white bread", - unit = "", - food = "", - disableAmount = false, - quantity = 2 ) - val FULL_CAKE_INFO_ENTITY = FullRecipeInfo( + val FULL_CAKE_INFO_ENTITY = FullRecipeEntity( recipeEntity = CAKE_RECIPE_ENTITY, recipeSummaryEntity = CAKE_RECIPE_SUMMARY_ENTITY, recipeIngredients = listOf( @@ -188,47 +138,35 @@ object RecipeImplTestData { ) private val PORRIDGE_RECIPE_ENTITY_FULL = RecipeEntity( - remoteId = 2, + remoteId = "2", recipeYield = "3 servings" ) private val PORRIDGE_MILK_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( localId = 4, - recipeId = 2, - title = "Milk", + recipeId = "2", note = "2 oz of white milk", - unit = "", - food = "", - disableAmount = true, - quantity = 3 ) private val PORRIDGE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( localId = 3, - recipeId = 2, - title = "Sugar", + recipeId = "2", note = "2 oz of white sugar", - unit = "", - food = "", - disableAmount = true, - quantity = 1 ) private val PORRIDGE_MIX_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( localId = 3, - recipeId = 2, - title = "Mix", + recipeId = "2", text = "Mix the ingredients" ) private val PORRIDGE_BOIL_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( localId = 4, - recipeId = 2, - title = "Boil", + recipeId = "2", text = "Boil the ingredients" ) - val FULL_PORRIDGE_INFO_ENTITY = FullRecipeInfo( + val FULL_PORRIDGE_INFO_ENTITY = FullRecipeEntity( recipeEntity = PORRIDGE_RECIPE_ENTITY_FULL, recipeSummaryEntity = PORRIDGE_RECIPE_SUMMARY_ENTITY, recipeIngredients = listOf( @@ -240,4 +178,21 @@ object RecipeImplTestData { PORRIDGE_BOIL_RECIPE_INSTRUCTION_ENTITY, ) ) + + val PORRIDGE_ADD_RECIPE_INFO = AddRecipeInfo( + name = "Porridge", + description = "Tasty breakfast", + recipeYield = "5 servings", + recipeIngredient = listOf( + AddRecipeIngredientInfo("Milk"), + AddRecipeIngredientInfo("Sugar"), + AddRecipeIngredientInfo("Salt"), + AddRecipeIngredientInfo("Porridge"), + ), + recipeInstructions = listOf( + AddRecipeInstructionInfo("Mix"), + AddRecipeInstructionInfo("Cook"), + ), + settings = AddRecipeSettingsInfo(disableComments = false, public = true), + ) } \ No newline at end of file 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 42afb57..d3efb46 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,8 +2,8 @@ package gq.kirmanak.mealient.ui.add import com.google.common.truth.Truth.assertThat import gq.kirmanak.mealient.data.add.AddRecipeRepo -import gq.kirmanak.mealient.datasource.models.AddRecipeRequest import gq.kirmanak.mealient.logging.Logger +import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_ADD_RECIPE_INFO import io.mockk.MockKAnnotations import io.mockk.coEvery import io.mockk.coVerify @@ -61,21 +61,21 @@ class AddRecipeViewModelTest { @Test fun `when preserve then doesn't update UI`() { - coEvery { addRecipeRepo.addRecipeRequestFlow } returns flowOf(AddRecipeRequest()) - subject.preserve(AddRecipeRequest()) + coEvery { addRecipeRepo.addRecipeRequestFlow } returns flowOf(PORRIDGE_ADD_RECIPE_INFO) + subject.preserve(PORRIDGE_ADD_RECIPE_INFO) coVerify(inverse = true) { addRecipeRepo.addRecipeRequestFlow } } @Test fun `when preservedAddRecipeRequest without loadPreservedRequest then empty`() = runTest { - coEvery { addRecipeRepo.addRecipeRequestFlow } returns flowOf(AddRecipeRequest()) + coEvery { addRecipeRepo.addRecipeRequestFlow } returns flowOf(PORRIDGE_ADD_RECIPE_INFO) val actual = withTimeoutOrNull(10) { subject.preservedAddRecipeRequest.firstOrNull() } assertThat(actual).isNull() } @Test fun `when loadPreservedRequest then updates preservedAddRecipeRequest`() = runTest { - val expected = AddRecipeRequest() + val expected = PORRIDGE_ADD_RECIPE_INFO coEvery { addRecipeRepo.addRecipeRequestFlow } returns flowOf(expected) subject.loadPreservedRequest() assertThat(subject.preservedAddRecipeRequest.first()).isSameInstanceAs(expected) @@ -83,7 +83,7 @@ class AddRecipeViewModelTest { @Test fun `when clear then updates preservedAddRecipeRequest`() = runTest { - val expected = AddRecipeRequest() + val expected = PORRIDGE_ADD_RECIPE_INFO coEvery { addRecipeRepo.addRecipeRequestFlow } returns flowOf(expected) subject.clear() assertThat(subject.preservedAddRecipeRequest.first()).isSameInstanceAs(expected) diff --git a/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt b/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt index b41b7fa..6cee162 100644 --- a/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt +++ b/app/src/test/java/gq/kirmanak/mealient/ui/baseurl/BaseURLViewModelTest.kt @@ -1,10 +1,11 @@ package gq.kirmanak.mealient.ui.baseurl -import gq.kirmanak.mealient.data.baseurl.BaseURLStorage +import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo import gq.kirmanak.mealient.data.baseurl.VersionDataSource import gq.kirmanak.mealient.data.baseurl.VersionInfo import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL +import gq.kirmanak.mealient.test.AuthImplTestData.TEST_VERSION import gq.kirmanak.mealient.test.RobolectricTest import io.mockk.MockKAnnotations import io.mockk.coEvery @@ -20,7 +21,7 @@ import org.junit.Test class BaseURLViewModelTest : RobolectricTest() { @MockK(relaxUnitFun = true) - lateinit var baseURLStorage: BaseURLStorage + lateinit var serverInfoRepo: ServerInfoRepo @MockK lateinit var versionDataSource: VersionDataSource @@ -33,16 +34,16 @@ class BaseURLViewModelTest : RobolectricTest() { @Before fun setUp() { MockKAnnotations.init(this) - subject = BaseURLViewModel(baseURLStorage, versionDataSource, logger) + subject = BaseURLViewModel(serverInfoRepo, versionDataSource, logger) } @Test fun `when saveBaseUrl and getVersionInfo returns result then saves to storage`() = runTest { coEvery { versionDataSource.getVersionInfo(eq(TEST_BASE_URL)) - } returns VersionInfo(true, "0.5.6", true) + } returns VersionInfo(TEST_VERSION) subject.saveBaseUrl(TEST_BASE_URL) advanceUntilIdle() - coVerify { baseURLStorage.storeBaseURL(eq(TEST_BASE_URL)) } + coVerify { serverInfoRepo.storeBaseURL(eq(TEST_BASE_URL), eq(TEST_VERSION)) } } } \ No newline at end of file diff --git a/database/schemas/gq.kirmanak.mealient.database.AppDb/3.json b/database/schemas/gq.kirmanak.mealient.database.AppDb/3.json new file mode 100644 index 0000000..9698138 --- /dev/null +++ b/database/schemas/gq.kirmanak.mealient.database.AppDb/3.json @@ -0,0 +1,404 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "28c896eb34e95c0cff33148178252f72", + "entities": [ + { + "tableName": "categories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "local_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_categories_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_categories_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "category_recipe", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` INTEGER NOT NULL, `recipe_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `recipe_id`), FOREIGN KEY(`category_id`) REFERENCES `categories`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipeId", + "columnName": "recipe_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "category_id", + "recipe_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_category_recipe_category_id_recipe_id", + "unique": true, + "columnNames": [ + "category_id", + "recipe_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_category_recipe_category_id_recipe_id` ON `${TABLE_NAME}` (`category_id`, `recipe_id`)" + }, + { + "name": "index_category_recipe_recipe_id", + "unique": false, + "columnNames": [ + "recipe_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_category_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)" + } + ], + "foreignKeys": [ + { + "table": "categories", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "category_id" + ], + "referencedColumns": [ + "local_id" + ] + }, + { + "table": "recipe_summaries", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "recipe_id" + ], + "referencedColumns": [ + "remote_id" + ] + } + ] + }, + { + "tableName": "tags", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "local_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_tags_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tags_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "tag_recipe", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag_id` INTEGER NOT NULL, `recipe_id` TEXT NOT NULL, PRIMARY KEY(`tag_id`, `recipe_id`), FOREIGN KEY(`tag_id`) REFERENCES `tags`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "tagId", + "columnName": "tag_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipeId", + "columnName": "recipe_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "tag_id", + "recipe_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tag_recipe_recipe_id", + "unique": false, + "columnNames": [ + "recipe_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tag_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)" + } + ], + "foreignKeys": [ + { + "table": "tags", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "tag_id" + ], + "referencedColumns": [ + "local_id" + ] + }, + { + "table": "recipe_summaries", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "recipe_id" + ], + "referencedColumns": [ + "remote_id" + ] + } + ] + }, + { + "tableName": "recipe_summaries", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `image` TEXT, `description` TEXT NOT NULL, `rating` INTEGER, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, PRIMARY KEY(`remote_id`))", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "slug", + "columnName": "slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rating", + "columnName": "rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateAdded", + "columnName": "date_added", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateUpdated", + "columnName": "date_updated", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "remote_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipe", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recipeYield", + "columnName": "recipe_yield", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "remote_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipe_ingredient", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `title` TEXT NOT NULL, `note` TEXT NOT NULL, `unit` TEXT NOT NULL, `food` TEXT NOT NULL, `disable_amount` INTEGER NOT NULL, `quantity` REAL NOT NULL)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipeId", + "columnName": "recipe_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unit", + "columnName": "unit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "food", + "columnName": "food", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "disableAmount", + "columnName": "disable_amount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "quantity", + "columnName": "quantity", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "local_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipe_instruction", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipeId", + "columnName": "recipe_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "local_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '28c896eb34e95c0cff33148178252f72')" + ] + } +} \ No newline at end of file diff --git a/database/schemas/gq.kirmanak.mealient.database.AppDb/4.json b/database/schemas/gq.kirmanak.mealient.database.AppDb/4.json new file mode 100644 index 0000000..6b273aa --- /dev/null +++ b/database/schemas/gq.kirmanak.mealient.database.AppDb/4.json @@ -0,0 +1,410 @@ +{ + "formatVersion": 1, + "database": { + "version": 4, + "identityHash": "13be83018f147e1f6e864790656da4a7", + "entities": [ + { + "tableName": "categories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "local_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_categories_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_categories_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "category_recipe", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` INTEGER NOT NULL, `recipe_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `recipe_id`), FOREIGN KEY(`category_id`) REFERENCES `categories`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipeId", + "columnName": "recipe_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "category_id", + "recipe_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_category_recipe_category_id_recipe_id", + "unique": true, + "columnNames": [ + "category_id", + "recipe_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_category_recipe_category_id_recipe_id` ON `${TABLE_NAME}` (`category_id`, `recipe_id`)" + }, + { + "name": "index_category_recipe_recipe_id", + "unique": false, + "columnNames": [ + "recipe_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_category_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)" + } + ], + "foreignKeys": [ + { + "table": "categories", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "category_id" + ], + "referencedColumns": [ + "local_id" + ] + }, + { + "table": "recipe_summaries", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "recipe_id" + ], + "referencedColumns": [ + "remote_id" + ] + } + ] + }, + { + "tableName": "tags", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "local_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_tags_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tags_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "tag_recipe", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag_id` INTEGER NOT NULL, `recipe_id` TEXT NOT NULL, PRIMARY KEY(`tag_id`, `recipe_id`), FOREIGN KEY(`tag_id`) REFERENCES `tags`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "tagId", + "columnName": "tag_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipeId", + "columnName": "recipe_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "tag_id", + "recipe_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tag_recipe_recipe_id", + "unique": false, + "columnNames": [ + "recipe_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tag_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)" + } + ], + "foreignKeys": [ + { + "table": "tags", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "tag_id" + ], + "referencedColumns": [ + "local_id" + ] + }, + { + "table": "recipe_summaries", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "recipe_id" + ], + "referencedColumns": [ + "remote_id" + ] + } + ] + }, + { + "tableName": "recipe_summaries", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `image` TEXT, `description` TEXT NOT NULL, `rating` INTEGER, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, `image_id` TEXT, PRIMARY KEY(`remote_id`))", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "slug", + "columnName": "slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rating", + "columnName": "rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateAdded", + "columnName": "date_added", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateUpdated", + "columnName": "date_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imageId", + "columnName": "image_id", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "remote_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipe", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recipeYield", + "columnName": "recipe_yield", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "remote_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipe_ingredient", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `title` TEXT NOT NULL, `note` TEXT NOT NULL, `unit` TEXT NOT NULL, `food` TEXT NOT NULL, `disable_amount` INTEGER NOT NULL, `quantity` REAL NOT NULL)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipeId", + "columnName": "recipe_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "unit", + "columnName": "unit", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "food", + "columnName": "food", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "disableAmount", + "columnName": "disable_amount", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "quantity", + "columnName": "quantity", + "affinity": "REAL", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "local_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipe_instruction", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `title` TEXT NOT NULL, `text` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipeId", + "columnName": "recipe_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "local_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '13be83018f147e1f6e864790656da4a7')" + ] + } +} \ No newline at end of file diff --git a/database/schemas/gq.kirmanak.mealient.database.AppDb/5.json b/database/schemas/gq.kirmanak.mealient.database.AppDb/5.json new file mode 100644 index 0000000..00986f8 --- /dev/null +++ b/database/schemas/gq.kirmanak.mealient.database.AppDb/5.json @@ -0,0 +1,374 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "e75a1e16503fdf60c62b7f9d17ec0bc6", + "entities": [ + { + "tableName": "categories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "local_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_categories_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_categories_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "category_recipe", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`category_id` INTEGER NOT NULL, `recipe_id` TEXT NOT NULL, PRIMARY KEY(`category_id`, `recipe_id`), FOREIGN KEY(`category_id`) REFERENCES `categories`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "categoryId", + "columnName": "category_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipeId", + "columnName": "recipe_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "category_id", + "recipe_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_category_recipe_category_id_recipe_id", + "unique": true, + "columnNames": [ + "category_id", + "recipe_id" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_category_recipe_category_id_recipe_id` ON `${TABLE_NAME}` (`category_id`, `recipe_id`)" + }, + { + "name": "index_category_recipe_recipe_id", + "unique": false, + "columnNames": [ + "recipe_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_category_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)" + } + ], + "foreignKeys": [ + { + "table": "categories", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "category_id" + ], + "referencedColumns": [ + "local_id" + ] + }, + { + "table": "recipe_summaries", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "recipe_id" + ], + "referencedColumns": [ + "remote_id" + ] + } + ] + }, + { + "tableName": "tags", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "local_id" + ], + "autoGenerate": true + }, + "indices": [ + { + "name": "index_tags_name", + "unique": true, + "columnNames": [ + "name" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_tags_name` ON `${TABLE_NAME}` (`name`)" + } + ], + "foreignKeys": [] + }, + { + "tableName": "tag_recipe", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`tag_id` INTEGER NOT NULL, `recipe_id` TEXT NOT NULL, PRIMARY KEY(`tag_id`, `recipe_id`), FOREIGN KEY(`tag_id`) REFERENCES `tags`(`local_id`) ON UPDATE CASCADE ON DELETE CASCADE , FOREIGN KEY(`recipe_id`) REFERENCES `recipe_summaries`(`remote_id`) ON UPDATE CASCADE ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "tagId", + "columnName": "tag_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipeId", + "columnName": "recipe_id", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "tag_id", + "recipe_id" + ], + "autoGenerate": false + }, + "indices": [ + { + "name": "index_tag_recipe_recipe_id", + "unique": false, + "columnNames": [ + "recipe_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_tag_recipe_recipe_id` ON `${TABLE_NAME}` (`recipe_id`)" + } + ], + "foreignKeys": [ + { + "table": "tags", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "tag_id" + ], + "referencedColumns": [ + "local_id" + ] + }, + { + "table": "recipe_summaries", + "onDelete": "CASCADE", + "onUpdate": "CASCADE", + "columns": [ + "recipe_id" + ], + "referencedColumns": [ + "remote_id" + ] + } + ] + }, + { + "tableName": "recipe_summaries", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `image` TEXT, `description` TEXT NOT NULL, `rating` INTEGER, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, `image_id` TEXT, PRIMARY KEY(`remote_id`))", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "slug", + "columnName": "slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "image", + "columnName": "image", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "rating", + "columnName": "rating", + "affinity": "INTEGER", + "notNull": false + }, + { + "fieldPath": "dateAdded", + "columnName": "date_added", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateUpdated", + "columnName": "date_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imageId", + "columnName": "image_id", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "remote_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipe", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recipeYield", + "columnName": "recipe_yield", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "remote_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipe_ingredient", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `note` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipeId", + "columnName": "recipe_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "local_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipe_instruction", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `text` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipeId", + "columnName": "recipe_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "local_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'e75a1e16503fdf60c62b7f9d17ec0bc6')" + ] + } +} \ No newline at end of file diff --git a/database/schemas/gq.kirmanak.mealient.database.AppDb/6.json b/database/schemas/gq.kirmanak.mealient.database.AppDb/6.json new file mode 100644 index 0000000..81e2dc6 --- /dev/null +++ b/database/schemas/gq.kirmanak.mealient.database.AppDb/6.json @@ -0,0 +1,160 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "f6e28dd617e4d4a6843a7865c9da736d", + "entities": [ + { + "tableName": "recipe_summaries", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `name` TEXT NOT NULL, `slug` TEXT NOT NULL, `description` TEXT NOT NULL, `date_added` INTEGER NOT NULL, `date_updated` INTEGER NOT NULL, `image_id` TEXT, PRIMARY KEY(`remote_id`))", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "slug", + "columnName": "slug", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "dateAdded", + "columnName": "date_added", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "dateUpdated", + "columnName": "date_updated", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "imageId", + "columnName": "image_id", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "columnNames": [ + "remote_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipe", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`remote_id` TEXT NOT NULL, `recipe_yield` TEXT NOT NULL, PRIMARY KEY(`remote_id`))", + "fields": [ + { + "fieldPath": "remoteId", + "columnName": "remote_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "recipeYield", + "columnName": "recipe_yield", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "remote_id" + ], + "autoGenerate": false + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipe_ingredient", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `note` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipeId", + "columnName": "recipe_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "note", + "columnName": "note", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "local_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipe_instruction", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`local_id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `recipe_id` TEXT NOT NULL, `text` TEXT NOT NULL)", + "fields": [ + { + "fieldPath": "localId", + "columnName": "local_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "recipeId", + "columnName": "recipe_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "text", + "columnName": "text", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "columnNames": [ + "local_id" + ], + "autoGenerate": true + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, 'f6e28dd617e4d4a6843a7865c9da736d')" + ] + } +} \ No newline at end of file diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt index 3f30751..37571cd 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/AppDb.kt @@ -1,19 +1,13 @@ package gq.kirmanak.mealient.database -import androidx.room.AutoMigration -import androidx.room.Database -import androidx.room.RoomDatabase -import androidx.room.TypeConverters +import androidx.room.* +import androidx.room.migration.AutoMigrationSpec import gq.kirmanak.mealient.database.recipe.RecipeDao import gq.kirmanak.mealient.database.recipe.entity.* @Database( - version = 2, + version = 6, entities = [ - CategoryEntity::class, - CategoryRecipeEntity::class, - TagEntity::class, - TagRecipeEntity::class, RecipeSummaryEntity::class, RecipeEntity::class, RecipeIngredientEntity::class, @@ -21,10 +15,29 @@ import gq.kirmanak.mealient.database.recipe.entity.* ], exportSchema = true, autoMigrations = [ - AutoMigration(from = 1, to = 2) + AutoMigration(from = 1, to = 2), + AutoMigration(from = 3, to = 4), + AutoMigration(from = 4, to = 5, spec = AppDb.From4To5Migration::class), + AutoMigration(from = 5, to = 6, spec = AppDb.From5To6Migration::class), ] ) @TypeConverters(RoomTypeConverters::class) abstract class AppDb : RoomDatabase() { abstract fun recipeDao(): RecipeDao + + @DeleteColumn(tableName = "recipe_instruction", columnName = "title") + @DeleteColumn(tableName = "recipe_ingredient", columnName = "title") + @DeleteColumn(tableName = "recipe_ingredient", columnName = "unit") + @DeleteColumn(tableName = "recipe_ingredient", columnName = "food") + @DeleteColumn(tableName = "recipe_ingredient", columnName = "disable_amount") + @DeleteColumn(tableName = "recipe_ingredient", columnName = "quantity") + class From4To5Migration : AutoMigrationSpec + + @DeleteColumn(tableName = "recipe_summaries", columnName = "image") + @DeleteColumn(tableName = "recipe_summaries", columnName = "rating") + @DeleteTable(tableName = "tag_recipe") + @DeleteTable(tableName = "tags") + @DeleteTable(tableName = "categories") + @DeleteTable(tableName = "category_recipe") + class From5To6Migration : AutoMigrationSpec } \ No newline at end of file diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/DatabaseModule.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/DatabaseModule.kt index 9fa9d7a..c9e96d2 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/DatabaseModule.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/DatabaseModule.kt @@ -17,6 +17,8 @@ interface DatabaseModule { @Provides @Singleton fun createDb(@ApplicationContext context: Context): AppDb = - Room.databaseBuilder(context, AppDb::class.java, "app.db").build() + Room.databaseBuilder(context, AppDb::class.java, "app.db") + .fallbackToDestructiveMigrationFrom(2) + .build() } } \ No newline at end of file diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeDao.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeDao.kt index 203953a..b3d1f72 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeDao.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/RecipeDao.kt @@ -6,54 +6,18 @@ import gq.kirmanak.mealient.database.recipe.entity.* @Dao interface RecipeDao { - @Query("SELECT * FROM tags") - suspend fun queryAllTags(): List - - @Query("SELECT * FROM categories") - suspend fun queryAllCategories(): List - @Query("SELECT * FROM recipe_summaries ORDER BY date_added DESC") fun queryRecipesByPages(): PagingSource @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertRecipe(recipeSummaryEntity: RecipeSummaryEntity) - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertTag(tagEntity: TagEntity): Long - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertTagRecipeEntity(tagRecipeEntity: TagRecipeEntity) - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertCategory(categoryEntity: CategoryEntity): Long - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertCategoryRecipeEntity(categoryRecipeEntity: CategoryRecipeEntity) - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertTagRecipeEntities(tagRecipeEntities: Set) - - @Insert(onConflict = OnConflictStrategy.IGNORE) - suspend fun insertCategoryRecipeEntities(categoryRecipeEntities: Set) - @Query("DELETE FROM recipe_summaries") suspend fun removeAllRecipes() - @Query("DELETE FROM tags") - suspend fun removeAllTags() - - @Query("DELETE FROM categories") - suspend fun removeAllCategories() - @Query("SELECT * FROM recipe_summaries ORDER BY date_updated DESC") suspend fun queryAllRecipes(): List - @Query("SELECT * FROM category_recipe") - suspend fun queryAllCategoryRecipes(): List - - @Query("SELECT * FROM tag_recipe") - suspend fun queryAllTagRecipes(): List - @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun insertRecipe(recipe: RecipeEntity) @@ -66,11 +30,11 @@ interface RecipeDao { @Transaction @SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) // The lint is wrong, the columns are actually used @Query("SELECT * FROM recipe JOIN recipe_summaries ON recipe.remote_id = recipe_summaries.remote_id JOIN recipe_ingredient ON recipe_ingredient.recipe_id = recipe.remote_id JOIN recipe_instruction ON recipe_instruction.recipe_id = recipe.remote_id WHERE recipe.remote_id = :recipeId") - suspend fun queryFullRecipeInfo(recipeId: Long): FullRecipeInfo? + suspend fun queryFullRecipeInfo(recipeId: String): FullRecipeEntity? @Query("DELETE FROM recipe_ingredient WHERE recipe_id = :recipeId") - suspend fun deleteRecipeIngredients(recipeId: Long) + suspend fun deleteRecipeIngredients(recipeId: String) @Query("DELETE FROM recipe_instruction WHERE recipe_id = :recipeId") - suspend fun deleteRecipeInstructions(recipeId: Long) + suspend fun deleteRecipeInstructions(recipeId: String) } \ No newline at end of file diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/CategoryEntity.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/CategoryEntity.kt deleted file mode 100644 index 4384ded..0000000 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/CategoryEntity.kt +++ /dev/null @@ -1,12 +0,0 @@ -package gq.kirmanak.mealient.database.recipe.entity - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.Index -import androidx.room.PrimaryKey - -@Entity(tableName = "categories", indices = [Index(value = ["name"], unique = true)]) -data class CategoryEntity( - @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0, - @ColumnInfo(name = "name") val name: String, -) \ No newline at end of file diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/CategoryRecipeEntity.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/CategoryRecipeEntity.kt deleted file mode 100644 index d0fca2d..0000000 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/CategoryRecipeEntity.kt +++ /dev/null @@ -1,29 +0,0 @@ -package gq.kirmanak.mealient.database.recipe.entity - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.ForeignKey -import androidx.room.Index - -@Entity( - tableName = "category_recipe", - primaryKeys = ["category_id", "recipe_id"], - indices = [Index(value = ["category_id", "recipe_id"], unique = true)], - foreignKeys = [ForeignKey( - entity = CategoryEntity::class, - parentColumns = ["local_id"], - childColumns = ["category_id"], - onDelete = ForeignKey.CASCADE, - onUpdate = ForeignKey.CASCADE - ), ForeignKey( - entity = RecipeSummaryEntity::class, - parentColumns = ["remote_id"], - childColumns = ["recipe_id"], - onDelete = ForeignKey.CASCADE, - onUpdate = ForeignKey.CASCADE - )] -) -data class CategoryRecipeEntity( - @ColumnInfo(name = "category_id") val categoryId: Long, - @ColumnInfo(name = "recipe_id", index = true) val recipeId: Long -) \ No newline at end of file diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/FullRecipeInfo.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/FullRecipeEntity.kt similarity index 95% rename from database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/FullRecipeInfo.kt rename to database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/FullRecipeEntity.kt index 27b02cc..2c0cab0 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/FullRecipeInfo.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/FullRecipeEntity.kt @@ -3,7 +3,7 @@ package gq.kirmanak.mealient.database.recipe.entity import androidx.room.Embedded import androidx.room.Relation -data class FullRecipeInfo( +data class FullRecipeEntity( @Embedded val recipeEntity: RecipeEntity, @Relation( parentColumn = "remote_id", diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeEntity.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeEntity.kt index 9db36b9..e4784a2 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeEntity.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeEntity.kt @@ -6,6 +6,6 @@ import androidx.room.PrimaryKey @Entity(tableName = "recipe") data class RecipeEntity( - @PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: Long, + @PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: String, @ColumnInfo(name = "recipe_yield") val recipeYield: String, ) diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeIngredientEntity.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeIngredientEntity.kt index e539674..31b7b6a 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeIngredientEntity.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeIngredientEntity.kt @@ -7,11 +7,6 @@ import androidx.room.PrimaryKey @Entity(tableName = "recipe_ingredient") data class RecipeIngredientEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0, - @ColumnInfo(name = "recipe_id") val recipeId: Long, - @ColumnInfo(name = "title") val title: String, + @ColumnInfo(name = "recipe_id") val recipeId: String, @ColumnInfo(name = "note") val note: String, - @ColumnInfo(name = "unit") val unit: String, - @ColumnInfo(name = "food") val food: String, - @ColumnInfo(name = "disable_amount") val disableAmount: Boolean, - @ColumnInfo(name = "quantity") val quantity: Int, ) \ No newline at end of file diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeInstructionEntity.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeInstructionEntity.kt index 8cbb8d6..c5905fa 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeInstructionEntity.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeInstructionEntity.kt @@ -7,7 +7,6 @@ import androidx.room.PrimaryKey @Entity(tableName = "recipe_instruction") data class RecipeInstructionEntity( @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0, - @ColumnInfo(name = "recipe_id") val recipeId: Long, - @ColumnInfo(name = "title") val title: String, + @ColumnInfo(name = "recipe_id") val recipeId: String, @ColumnInfo(name = "text") val text: String, ) diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryEntity.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryEntity.kt index dfb3cce..afbc99d 100644 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryEntity.kt +++ b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/RecipeSummaryEntity.kt @@ -8,16 +8,11 @@ import kotlinx.datetime.LocalDateTime @Entity(tableName = "recipe_summaries") data class RecipeSummaryEntity( - @PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: Long, + @PrimaryKey @ColumnInfo(name = "remote_id") val remoteId: String, @ColumnInfo(name = "name") val name: String, @ColumnInfo(name = "slug") val slug: String, - @ColumnInfo(name = "image") val image: String?, @ColumnInfo(name = "description") val description: String, - @ColumnInfo(name = "rating") val rating: Int?, @ColumnInfo(name = "date_added") val dateAdded: LocalDate, - @ColumnInfo(name = "date_updated") val dateUpdated: LocalDateTime -) { - override fun toString(): String { - return "RecipeSummaryEntity(remoteId=$remoteId, name='$name')" - } -} \ No newline at end of file + @ColumnInfo(name = "date_updated") val dateUpdated: LocalDateTime, + @ColumnInfo(name = "image_id") val imageId: String?, +) \ No newline at end of file diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/TagEntity.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/TagEntity.kt deleted file mode 100644 index 460c649..0000000 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/TagEntity.kt +++ /dev/null @@ -1,12 +0,0 @@ -package gq.kirmanak.mealient.database.recipe.entity - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.Index -import androidx.room.PrimaryKey - -@Entity(tableName = "tags", indices = [Index(value = ["name"], unique = true)]) -data class TagEntity( - @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0, - @ColumnInfo(name = "name") val name: String -) \ No newline at end of file diff --git a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/TagRecipeEntity.kt b/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/TagRecipeEntity.kt deleted file mode 100644 index 332a5b1..0000000 --- a/database/src/main/kotlin/gq/kirmanak/mealient/database/recipe/entity/TagRecipeEntity.kt +++ /dev/null @@ -1,27 +0,0 @@ -package gq.kirmanak.mealient.database.recipe.entity - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.ForeignKey - -@Entity( - tableName = "tag_recipe", - primaryKeys = ["tag_id", "recipe_id"], - foreignKeys = [ForeignKey( - entity = TagEntity::class, - parentColumns = ["local_id"], - childColumns = ["tag_id"], - onDelete = ForeignKey.CASCADE, - onUpdate = ForeignKey.CASCADE - ), ForeignKey( - entity = RecipeSummaryEntity::class, - parentColumns = ["remote_id"], - childColumns = ["recipe_id"], - onDelete = ForeignKey.CASCADE, - onUpdate = ForeignKey.CASCADE - )] -) -data class TagRecipeEntity( - @ColumnInfo(name = "tag_id") val tagId: Long, - @ColumnInfo(name = "recipe_id", index = true) val recipeId: Long -) \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceExtensions.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceExtensions.kt new file mode 100644 index 0000000..4b7f291 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceExtensions.kt @@ -0,0 +1,22 @@ +package gq.kirmanak.mealient.datasource + +import kotlinx.coroutines.CancellationException +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.decodeFromStream +import okhttp3.ResponseBody + +/** + * Like [runCatching] but rethrows [CancellationException] to support + * cancellation of coroutines. + */ +inline fun runCatchingExceptCancel(block: () -> T): Result = try { + Result.success(block()) +} catch (e: CancellationException) { + throw e +} catch (e: Throwable) { + Result.failure(e) +} + +@OptIn(ExperimentalSerializationApi::class) +inline fun ResponseBody.decode(json: Json): R = json.decodeFromStream(byteStream()) diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceModule.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceModule.kt index 8d29930..afaf344 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceModule.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceModule.kt @@ -6,6 +6,12 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0 +import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0Impl +import gq.kirmanak.mealient.datasource.v0.MealieServiceV0 +import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1 +import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1Impl +import gq.kirmanak.mealient.datasource.v1.MealieServiceV1 import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.json.Json import okhttp3.MediaType.Companion.toMediaType @@ -49,9 +55,13 @@ interface DataSourceModule { @Provides @Singleton - fun provideMealieService(retrofit: Retrofit): MealieService = + fun provideMealieService(retrofit: Retrofit): MealieServiceV0 = retrofit.create() + @Provides + @Singleton + fun provideMealieServiceV1(retrofit: Retrofit): MealieServiceV1 = + retrofit.create() } @Binds @@ -64,5 +74,13 @@ interface DataSourceModule { @Binds @Singleton - fun bindMealieDataSource(mealientDataSourceImpl: MealieDataSourceImpl): MealieDataSource + fun bindMealieDataSource(mealientDataSourceImpl: MealieDataSourceV0Impl): MealieDataSourceV0 + + @Binds + @Singleton + fun bindMealieDataSourceV1(mealientDataSourceImpl: MealieDataSourceV1Impl): MealieDataSourceV1 + + @Binds + @Singleton + fun bindNetworkRequestWrapper(networkRequestWrapperImpl: NetworkRequestWrapperImpl): NetworkRequestWrapper } \ 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 deleted file mode 100644 index 31cc9ad..0000000 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt +++ /dev/null @@ -1,92 +0,0 @@ -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/models/NetworkError.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/NetworkError.kt similarity index 87% rename from datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/NetworkError.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/NetworkError.kt index 7fcd669..50953a4 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/NetworkError.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/NetworkError.kt @@ -1,4 +1,4 @@ -package gq.kirmanak.mealient.datasource.models +package gq.kirmanak.mealient.datasource sealed class NetworkError(cause: Throwable) : RuntimeException(cause) { class Unauthorized(cause: Throwable) : NetworkError(cause) diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/NetworkRequestWrapper.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/NetworkRequestWrapper.kt new file mode 100644 index 0000000..8a8dd01 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/NetworkRequestWrapper.kt @@ -0,0 +1,17 @@ +package gq.kirmanak.mealient.datasource + +interface NetworkRequestWrapper { + + suspend fun makeCall( + block: suspend () -> T, + logMethod: () -> String, + logParameters: () -> String, + ): Result + + suspend fun makeCallAndHandleUnauthorized( + block: suspend () -> T, + logMethod: () -> String, + logParameters: () -> String, + ): T + +} \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/NetworkRequestWrapperImpl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/NetworkRequestWrapperImpl.kt new file mode 100644 index 0000000..5bf01dc --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/NetworkRequestWrapperImpl.kt @@ -0,0 +1,36 @@ +package gq.kirmanak.mealient.datasource + +import gq.kirmanak.mealient.logging.Logger +import retrofit2.HttpException +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class NetworkRequestWrapperImpl @Inject constructor( + private val logger: Logger, +) : NetworkRequestWrapper { + + override suspend fun makeCall( + block: suspend () -> T, + logMethod: () -> String, + logParameters: () -> String, + ): Result { + logger.v { "${logMethod()} called with: ${logParameters()}" } + return runCatchingExceptCancel { block() } + .onFailure { logger.e(it) { "${logMethod()} request failed with: ${logParameters()}" } } + .onSuccess { logger.d { "${logMethod()} request succeeded with ${logParameters()}, result = $it" } } + } + + override suspend fun makeCallAndHandleUnauthorized( + block: suspend () -> T, + logMethod: () -> String, + logParameters: () -> String + ): T = makeCall(block, logMethod, logParameters).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/models/AddRecipeRequest.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/AddRecipeRequest.kt deleted file mode 100644 index 9c2f34b..0000000 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/AddRecipeRequest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package gq.kirmanak.mealient.datasource.models - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class AddRecipeRequest( - @SerialName("name") val name: String = "", - @SerialName("description") val description: String = "", - @SerialName("image") val image: String = "", - @SerialName("recipeYield") val recipeYield: String = "", - @SerialName("recipeIngredient") val recipeIngredient: List = emptyList(), - @SerialName("recipeInstructions") val recipeInstructions: List = emptyList(), - @SerialName("slug") val slug: String = "", - @SerialName("filePath") val filePath: String = "", - @SerialName("tags") val tags: List = emptyList(), - @SerialName("categories") val categories: List = emptyList(), - @SerialName("notes") val notes: List = emptyList(), - @SerialName("extras") val extras: Map = emptyMap(), - @SerialName("assets") val assets: List = emptyList(), - @SerialName("settings") val settings: AddRecipeSettings = AddRecipeSettings(), -) - -@Serializable -data class AddRecipeSettings( - @SerialName("disableAmount") val disableAmount: Boolean = true, - @SerialName("disableComments") val disableComments: Boolean = false, - @SerialName("landscapeView") val landscapeView: Boolean = true, - @SerialName("public") val public: Boolean = true, - @SerialName("showAssets") val showAssets: Boolean = true, - @SerialName("showNutrition") val showNutrition: Boolean = true, -) - -@Serializable -data class AddRecipeNote( - @SerialName("title") val title: String = "", - @SerialName("text") val text: String = "", -) - -@Serializable -data class AddRecipeInstruction( - @SerialName("title") val title: String = "", - @SerialName("text") val text: String = "", -) - -@Serializable -data class AddRecipeIngredient( - @SerialName("disableAmount") val disableAmount: Boolean = true, - @SerialName("food") val food: String? = null, - @SerialName("note") val note: String = "", - @SerialName("quantity") val quantity: Int = 1, - @SerialName("title") val title: String? = null, - @SerialName("unit") val unit: String? = null, -) diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/ErrorDetail.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/ErrorDetail.kt deleted file mode 100644 index 00efd12..0000000 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/ErrorDetail.kt +++ /dev/null @@ -1,7 +0,0 @@ -package gq.kirmanak.mealient.datasource.models - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class ErrorDetail(@SerialName("detail") val detail: String? = null) \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeIngredientResponse.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeIngredientResponse.kt deleted file mode 100644 index e00a9fb..0000000 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeIngredientResponse.kt +++ /dev/null @@ -1,14 +0,0 @@ -package gq.kirmanak.mealient.datasource.models - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class GetRecipeIngredientResponse( - @SerialName("title") val title: String = "", - @SerialName("note") val note: String = "", - @SerialName("unit") val unit: String = "", - @SerialName("food") val food: String = "", - @SerialName("disableAmount") val disableAmount: Boolean, - @SerialName("quantity") val quantity: Int, -) diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeInstructionResponse.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeInstructionResponse.kt deleted file mode 100644 index c6c2fe7..0000000 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeInstructionResponse.kt +++ /dev/null @@ -1,10 +0,0 @@ -package gq.kirmanak.mealient.datasource.models - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class GetRecipeInstructionResponse( - @SerialName("title") val title: String = "", - @SerialName("text") val text: String, -) diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeResponse.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeResponse.kt deleted file mode 100644 index 3197d75..0000000 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeResponse.kt +++ /dev/null @@ -1,23 +0,0 @@ -package gq.kirmanak.mealient.datasource.models - -import kotlinx.datetime.LocalDate -import kotlinx.datetime.LocalDateTime -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class GetRecipeResponse( - @SerialName("id") val remoteId: Long, - @SerialName("name") val name: String, - @SerialName("slug") val slug: String, - @SerialName("image") val image: String, - @SerialName("description") val description: String = "", - @SerialName("recipeCategory") val recipeCategories: List, - @SerialName("tags") val tags: List, - @SerialName("rating") val rating: Int?, - @SerialName("dateAdded") val dateAdded: LocalDate, - @SerialName("dateUpdated") val dateUpdated: LocalDateTime, - @SerialName("recipeYield") val recipeYield: String = "", - @SerialName("recipeIngredient") val recipeIngredients: List, - @SerialName("recipeInstructions") val recipeInstructions: List, -) diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeSummaryResponse.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeSummaryResponse.kt deleted file mode 100644 index f9fc613..0000000 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeSummaryResponse.kt +++ /dev/null @@ -1,24 +0,0 @@ -package gq.kirmanak.mealient.datasource.models - -import kotlinx.datetime.LocalDate -import kotlinx.datetime.LocalDateTime -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class GetRecipeSummaryResponse( - @SerialName("id") val remoteId: Long, - @SerialName("name") val name: String, - @SerialName("slug") val slug: String, - @SerialName("image") val image: String?, - @SerialName("description") val description: String = "", - @SerialName("recipeCategory") val recipeCategories: List, - @SerialName("tags") val tags: List, - @SerialName("rating") val rating: Int?, - @SerialName("dateAdded") val dateAdded: LocalDate, - @SerialName("dateUpdated") val dateUpdated: LocalDateTime -) { - override fun toString(): String { - return "GetRecipeSummaryResponse(remoteId=$remoteId, name='$name')" - } -} \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetTokenResponse.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetTokenResponse.kt deleted file mode 100644 index 66a79db..0000000 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetTokenResponse.kt +++ /dev/null @@ -1,7 +0,0 @@ -package gq.kirmanak.mealient.datasource.models - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class GetTokenResponse(@SerialName("access_token") val accessToken: String) \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/VersionResponse.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/VersionResponse.kt deleted file mode 100644 index 44f0a09..0000000 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/VersionResponse.kt +++ /dev/null @@ -1,14 +0,0 @@ -package gq.kirmanak.mealient.datasource.models - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable - -@Serializable -data class VersionResponse( - @SerialName("production") - val production: Boolean, - @SerialName("version") - val version: String, - @SerialName("demoStatus") - val demoStatus: Boolean, -) \ 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/v0/MealieDataSourceV0.kt similarity index 55% rename from datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSource.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0.kt index c8fd1ae..a51d5a7 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSource.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0.kt @@ -1,16 +1,16 @@ -package gq.kirmanak.mealient.datasource +package gq.kirmanak.mealient.datasource.v0 -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 +import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0 +import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.VersionResponseV0 -interface MealieDataSource { +interface MealieDataSourceV0 { suspend fun addRecipe( baseUrl: String, token: String?, - recipe: AddRecipeRequest, + recipe: AddRecipeRequestV0, ): String /** @@ -24,18 +24,18 @@ interface MealieDataSource { suspend fun getVersionInfo( baseUrl: String, - ): VersionResponse + ): VersionResponseV0 suspend fun requestRecipes( baseUrl: String, token: String?, start: Int, limit: Int, - ): List + ): List suspend fun requestRecipeInfo( baseUrl: String, token: String?, slug: String, - ): GetRecipeResponse + ): GetRecipeResponseV0 } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0Impl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0Impl.kt new file mode 100644 index 0000000..db7b276 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieDataSourceV0Impl.kt @@ -0,0 +1,80 @@ +package gq.kirmanak.mealient.datasource.v0 + +import gq.kirmanak.mealient.datasource.NetworkError +import gq.kirmanak.mealient.datasource.NetworkRequestWrapper +import gq.kirmanak.mealient.datasource.decode +import gq.kirmanak.mealient.datasource.v0.models.* +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import retrofit2.HttpException +import java.net.ConnectException +import java.net.SocketTimeoutException +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MealieDataSourceV0Impl @Inject constructor( + private val networkRequestWrapper: NetworkRequestWrapper, + private val service: MealieServiceV0, + private val json: Json, +) : MealieDataSourceV0 { + + override suspend fun addRecipe( + baseUrl: String, + token: String?, + recipe: AddRecipeRequestV0, + ): String = networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.addRecipe("$baseUrl/api/recipes/create", token, recipe) }, + logMethod = { "addRecipe" }, + logParameters = { "baseUrl = $baseUrl, token = $token, recipe = $recipe" } + ) + + override suspend fun authenticate( + baseUrl: String, + username: String, + password: String, + ): String = networkRequestWrapper.makeCall( + block = { service.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 errorDetailV0 = errorBody.decode(json) + throw if (errorDetailV0.detail == "Unauthorized") NetworkError.Unauthorized(it) else it + } + + override suspend fun getVersionInfo( + baseUrl: String + ): VersionResponseV0 = networkRequestWrapper.makeCall( + block = { service.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 = networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.getRecipeSummary("$baseUrl/api/recipes/summary", token, start, limit) }, + logMethod = { "requestRecipes" }, + logParameters = { "baseUrl = $baseUrl, token = $token, start = $start, limit = $limit" } + ) + + override suspend fun requestRecipeInfo( + baseUrl: String, + token: String?, + slug: String, + ): GetRecipeResponseV0 = networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.getRecipe("$baseUrl/api/recipes/$slug", token) }, + logMethod = { "requestRecipeInfo" }, + logParameters = { "baseUrl = $baseUrl, token = $token, slug = $slug" } + ) +} diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieServiceV0.kt similarity index 73% rename from datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieServiceV0.kt index 9750cf9..0d6329b 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/MealieServiceV0.kt @@ -1,10 +1,10 @@ -package gq.kirmanak.mealient.datasource +package gq.kirmanak.mealient.datasource.v0 import gq.kirmanak.mealient.datasource.DataSourceModule.Companion.AUTHORIZATION_HEADER_NAME -import gq.kirmanak.mealient.datasource.models.* +import gq.kirmanak.mealient.datasource.v0.models.* import retrofit2.http.* -interface MealieService { +interface MealieServiceV0 { @FormUrlEncoded @POST @@ -12,19 +12,19 @@ interface MealieService { @Url url: String, @Field("username") username: String, @Field("password") password: String, - ): GetTokenResponse + ): GetTokenResponseV0 @POST suspend fun addRecipe( @Url url: String, @Header(AUTHORIZATION_HEADER_NAME) token: String?, - @Body addRecipeRequest: AddRecipeRequest, + @Body addRecipeRequestV0: AddRecipeRequestV0, ): String @GET suspend fun getVersion( @Url url: String, - ): VersionResponse + ): VersionResponseV0 @GET suspend fun getRecipeSummary( @@ -32,11 +32,11 @@ interface MealieService { @Header(AUTHORIZATION_HEADER_NAME) token: String?, @Query("start") start: Int, @Query("limit") limit: Int, - ): List + ): List @GET suspend fun getRecipe( @Url url: String, @Header(AUTHORIZATION_HEADER_NAME) token: String?, - ): GetRecipeResponse + ): GetRecipeResponseV0 } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/AddRecipeRequestV0.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/AddRecipeRequestV0.kt new file mode 100644 index 0000000..fa6e6c7 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/AddRecipeRequestV0.kt @@ -0,0 +1,30 @@ +package gq.kirmanak.mealient.datasource.v0.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class AddRecipeRequestV0( + @SerialName("name") val name: String, + @SerialName("description") val description: String, + @SerialName("recipeYield") val recipeYield: String, + @SerialName("recipeIngredient") val recipeIngredient: List, + @SerialName("recipeInstructions") val recipeInstructions: List, + @SerialName("settings") val settings: AddRecipeSettingsV0, +) + +@Serializable +data class AddRecipeIngredientV0( + @SerialName("note") val note: String, +) + +@Serializable +data class AddRecipeInstructionV0( + @SerialName("text") val text: String, +) + +@Serializable +data class AddRecipeSettingsV0( + @SerialName("disableComments") val disableComments: Boolean, + @SerialName("public") val public: Boolean, +) \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/ErrorDetailV0.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/ErrorDetailV0.kt new file mode 100644 index 0000000..50bce5e --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/ErrorDetailV0.kt @@ -0,0 +1,7 @@ +package gq.kirmanak.mealient.datasource.v0.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ErrorDetailV0(@SerialName("detail") val detail: String? = null) \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/GetRecipeResponseV0.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/GetRecipeResponseV0.kt new file mode 100644 index 0000000..2930c61 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/GetRecipeResponseV0.kt @@ -0,0 +1,23 @@ +package gq.kirmanak.mealient.datasource.v0.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetRecipeResponseV0( + @SerialName("id") val remoteId: Int, + @SerialName("name") val name: String, + @SerialName("recipeYield") val recipeYield: String = "", + @SerialName("recipeIngredient") val recipeIngredients: List, + @SerialName("recipeInstructions") val recipeInstructions: List, +) + +@Serializable +data class GetRecipeIngredientResponseV0( + @SerialName("note") val note: String = "", +) + +@Serializable +data class GetRecipeInstructionResponseV0( + @SerialName("text") val text: String, +) \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/GetRecipeSummaryResponseV0.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/GetRecipeSummaryResponseV0.kt new file mode 100644 index 0000000..cc28c55 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/GetRecipeSummaryResponseV0.kt @@ -0,0 +1,16 @@ +package gq.kirmanak.mealient.datasource.v0.models + +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetRecipeSummaryResponseV0( + @SerialName("id") val remoteId: Int, + @SerialName("name") val name: String, + @SerialName("slug") val slug: String, + @SerialName("description") val description: String = "", + @SerialName("dateAdded") val dateAdded: LocalDate, + @SerialName("dateUpdated") val dateUpdated: LocalDateTime +) \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/GetTokenResponseV0.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/GetTokenResponseV0.kt new file mode 100644 index 0000000..2e4cfa9 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/GetTokenResponseV0.kt @@ -0,0 +1,9 @@ +package gq.kirmanak.mealient.datasource.v0.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetTokenResponseV0( + @SerialName("access_token") val accessToken: String, +) \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/VersionResponseV0.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/VersionResponseV0.kt new file mode 100644 index 0000000..590e5d4 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v0/models/VersionResponseV0.kt @@ -0,0 +1,9 @@ +package gq.kirmanak.mealient.datasource.v0.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class VersionResponseV0( + @SerialName("version") val version: String, +) \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt new file mode 100644 index 0000000..6ba2d6b --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt @@ -0,0 +1,45 @@ +package gq.kirmanak.mealient.datasource.v1 + +import gq.kirmanak.mealient.datasource.v1.models.* + +interface MealieDataSourceV1 { + + suspend fun createRecipe( + baseUrl: String, + token: String?, + recipe: CreateRecipeRequestV1, + ): String + + suspend fun updateRecipe( + baseUrl: String, + token: String?, + slug: String, + recipe: UpdateRecipeRequestV1, + ): GetRecipeResponseV1 + + /** + * Tries to acquire authentication token using the provided credentials + */ + suspend fun authenticate( + baseUrl: String, + username: String, + password: String, + ): String + + suspend fun getVersionInfo( + baseUrl: String, + ): VersionResponseV1 + + suspend fun requestRecipes( + baseUrl: String, + token: String?, + page: Int, + perPage: Int, + ): List + + suspend fun requestRecipeInfo( + baseUrl: String, + token: String?, + slug: String, + ): GetRecipeResponseV1 +} \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt new file mode 100644 index 0000000..e00c0d0 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt @@ -0,0 +1,93 @@ +package gq.kirmanak.mealient.datasource.v1 + +import gq.kirmanak.mealient.datasource.NetworkError +import gq.kirmanak.mealient.datasource.NetworkRequestWrapper +import gq.kirmanak.mealient.datasource.decode +import gq.kirmanak.mealient.datasource.v1.models.* +import kotlinx.serialization.SerializationException +import kotlinx.serialization.json.Json +import retrofit2.HttpException +import java.net.ConnectException +import java.net.SocketTimeoutException +import javax.inject.Inject +import javax.inject.Singleton + +@Singleton +class MealieDataSourceV1Impl @Inject constructor( + private val networkRequestWrapper: NetworkRequestWrapper, + private val service: MealieServiceV1, + private val json: Json, +) : MealieDataSourceV1 { + + override suspend fun createRecipe( + baseUrl: String, + token: String?, + recipe: CreateRecipeRequestV1 + ): String = networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.createRecipe("$baseUrl/api/recipes", token, recipe) }, + logMethod = { "createRecipe" }, + logParameters = { "baseUrl = $baseUrl, token = $token, recipe = $recipe" } + ) + + override suspend fun updateRecipe( + baseUrl: String, + token: String?, + slug: String, + recipe: UpdateRecipeRequestV1 + ): GetRecipeResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.updateRecipe("$baseUrl/api/recipes/$slug", token, recipe) }, + logMethod = { "updateRecipe" }, + logParameters = { "baseUrl = $baseUrl, token = $token, slug = $slug, recipe = $recipe" } + ) + + override suspend fun authenticate( + baseUrl: String, + username: String, + password: String, + ): String = networkRequestWrapper.makeCall( + block = { service.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 errorDetailV0 = errorBody.decode(json) + throw if (errorDetailV0.detail == "Unauthorized") NetworkError.Unauthorized(it) else it + } + + override suspend fun getVersionInfo( + baseUrl: String, + ): VersionResponseV1 = networkRequestWrapper.makeCall( + block = { service.getVersion("$baseUrl/api/app/about") }, + logMethod = { "getVersionInfo" }, + logParameters = { "baseUrl = $baseUrl" }, + ).getOrElse { + throw when (it) { + is HttpException, is SerializationException -> NetworkError.NotMealie(it) + is SocketTimeoutException, is ConnectException -> NetworkError.NoServerConnection(it) + else -> NetworkError.MalformedUrl(it) + } + } + + override suspend fun requestRecipes( + baseUrl: String, + token: String?, + page: Int, + perPage: Int + ): List = networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.getRecipeSummary("$baseUrl/api/recipes", token, page, perPage) }, + logMethod = { "requestRecipes" }, + logParameters = { "baseUrl = $baseUrl, token = $token, page = $page, perPage = $perPage" } + ).items + + override suspend fun requestRecipeInfo( + baseUrl: String, + token: String?, + slug: String + ): GetRecipeResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized( + block = { service.getRecipe("$baseUrl/api/recipes/$slug", token) }, + logMethod = { "requestRecipeInfo" }, + logParameters = { "baseUrl = $baseUrl, token = $token, slug = $slug" } + ) + +} + diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt new file mode 100644 index 0000000..c6644fc --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt @@ -0,0 +1,49 @@ +package gq.kirmanak.mealient.datasource.v1 + +import gq.kirmanak.mealient.datasource.DataSourceModule.Companion.AUTHORIZATION_HEADER_NAME +import gq.kirmanak.mealient.datasource.v1.models.* +import retrofit2.http.* + +interface MealieServiceV1 { + + @FormUrlEncoded + @POST + suspend fun getToken( + @Url url: String, + @Field("username") username: String, + @Field("password") password: String, + ): GetTokenResponseV1 + + @POST + suspend fun createRecipe( + @Url url: String, + @Header(AUTHORIZATION_HEADER_NAME) token: String?, + @Body addRecipeRequest: CreateRecipeRequestV1, + ): String + + @PATCH + suspend fun updateRecipe( + @Url url: String, + @Header(AUTHORIZATION_HEADER_NAME) token: String?, + @Body addRecipeRequest: UpdateRecipeRequestV1, + ): GetRecipeResponseV1 + + @GET + suspend fun getVersion( + @Url url: String, + ): VersionResponseV1 + + @GET + suspend fun getRecipeSummary( + @Url url: String, + @Header(AUTHORIZATION_HEADER_NAME) token: String?, + @Query("page") page: Int, + @Query("perPage") perPage: Int, + ): GetRecipesResponseV1 + + @GET + suspend fun getRecipe( + @Url url: String, + @Header(AUTHORIZATION_HEADER_NAME) token: String?, + ): GetRecipeResponseV1 +} \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/CreateRecipeRequestV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/CreateRecipeRequestV1.kt new file mode 100644 index 0000000..bf5bc90 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/CreateRecipeRequestV1.kt @@ -0,0 +1,9 @@ +package gq.kirmanak.mealient.datasource.v1.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class CreateRecipeRequestV1( + @SerialName("name") val name: String, +) diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/ErrorDetailV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/ErrorDetailV1.kt new file mode 100644 index 0000000..54f3370 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/ErrorDetailV1.kt @@ -0,0 +1,9 @@ +package gq.kirmanak.mealient.datasource.v1.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class ErrorDetailV1( + @SerialName("detail") val detail: String? = null, +) \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeResponseV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeResponseV1.kt new file mode 100644 index 0000000..87fdd31 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeResponseV1.kt @@ -0,0 +1,23 @@ +package gq.kirmanak.mealient.datasource.v1.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetRecipeResponseV1( + @SerialName("id") val remoteId: String, + @SerialName("name") val name: String, + @SerialName("recipeYield") val recipeYield: String = "", + @SerialName("recipeIngredient") val recipeIngredients: List, + @SerialName("recipeInstructions") val recipeInstructions: List, +) + +@Serializable +data class GetRecipeIngredientResponseV1( + @SerialName("note") val note: String = "", +) + +@Serializable +data class GetRecipeInstructionResponseV1( + @SerialName("text") val text: String, +) \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeSummaryResponseV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeSummaryResponseV1.kt new file mode 100644 index 0000000..f4512b8 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeSummaryResponseV1.kt @@ -0,0 +1,16 @@ +package gq.kirmanak.mealient.datasource.v1.models + +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetRecipeSummaryResponseV1( + @SerialName("id") val remoteId: String, + @SerialName("name") val name: String, + @SerialName("slug") val slug: String, + @SerialName("description") val description: String = "", + @SerialName("dateAdded") val dateAdded: LocalDate, + @SerialName("dateUpdated") val dateUpdated: LocalDateTime +) \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipesResponseV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipesResponseV1.kt new file mode 100644 index 0000000..c6e33f0 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipesResponseV1.kt @@ -0,0 +1,9 @@ +package gq.kirmanak.mealient.datasource.v1.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetRecipesResponseV1( + @SerialName("items") val items: List, +) diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetTokenResponseV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetTokenResponseV1.kt new file mode 100644 index 0000000..11e96f8 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetTokenResponseV1.kt @@ -0,0 +1,9 @@ +package gq.kirmanak.mealient.datasource.v1.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetTokenResponseV1( + @SerialName("access_token") val accessToken: String, +) \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/UpdateRecipeRequestV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/UpdateRecipeRequestV1.kt new file mode 100644 index 0000000..3df266b --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/UpdateRecipeRequestV1.kt @@ -0,0 +1,32 @@ +package gq.kirmanak.mealient.datasource.v1.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class UpdateRecipeRequestV1( + @SerialName("description") val description: String, + @SerialName("recipeYield") val recipeYield: String, + @SerialName("recipeIngredient") val recipeIngredient: List, + @SerialName("recipeInstructions") val recipeInstructions: List, + @SerialName("settings") val settings: AddRecipeSettingsV1, +) + +@Serializable +data class AddRecipeIngredientV1( + @SerialName("referenceId") val id: String, + @SerialName("note") val note: String, +) + +@Serializable +data class AddRecipeInstructionV1( + @SerialName("id") val id: String, + @SerialName("text") val text: String = "", + @SerialName("ingredientReferences") val ingredientReferences: List, +) + +@Serializable +data class AddRecipeSettingsV1( + @SerialName("disableComments") val disableComments: Boolean, + @SerialName("public") val public: Boolean, +) \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/VersionResponseV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/VersionResponseV1.kt new file mode 100644 index 0000000..45f5cb5 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/VersionResponseV1.kt @@ -0,0 +1,9 @@ +package gq.kirmanak.mealient.datasource.v1.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class VersionResponseV1( + @SerialName("version") val version: String, +) \ No newline at end of file diff --git a/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImplTest.kt b/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceV0ImplTest.kt similarity index 80% rename from datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImplTest.kt rename to datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceV0ImplTest.kt index fa82b9b..e43d745 100644 --- a/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImplTest.kt +++ b/datasource/src/test/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceV0ImplTest.kt @@ -1,9 +1,10 @@ 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.datasource.v0.MealieDataSourceV0Impl +import gq.kirmanak.mealient.datasource.v0.MealieServiceV0 +import gq.kirmanak.mealient.datasource.v0.models.GetTokenResponseV0 +import gq.kirmanak.mealient.datasource.v0.models.VersionResponseV0 import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.test.toJsonResponseBody import io.mockk.MockKAnnotations @@ -21,25 +22,26 @@ import java.io.IOException import java.net.ConnectException @OptIn(ExperimentalCoroutinesApi::class) -class MealieDataSourceImplTest { +class MealieDataSourceV0ImplTest { @MockK - lateinit var service: MealieService + lateinit var service: MealieServiceV0 @MockK(relaxUnitFun = true) lateinit var logger: Logger - lateinit var subject: MealieDataSourceImpl + lateinit var subject: MealieDataSourceV0Impl @Before fun setUp() { MockKAnnotations.init(this) - subject = MealieDataSourceImpl(logger, service, Json.Default) + val networkRequestWrapper: NetworkRequestWrapper = NetworkRequestWrapperImpl(logger) + subject = MealieDataSourceV0Impl(networkRequestWrapper, service, Json) } @Test(expected = NetworkError.NotMealie::class) fun `when getVersionInfo and getVersion throws HttpException then NotMealie`() = runTest { - val error = HttpException(Response.error(404, "".toJsonResponseBody())) + val error = HttpException(Response.error(404, "".toJsonResponseBody())) coEvery { service.getVersion(any()) } throws error subject.getVersionInfo(TEST_BASE_URL) } @@ -60,14 +62,14 @@ class MealieDataSourceImplTest { @Test fun `when getVersionInfo and getVersion returns result then result`() = runTest { - val versionResponse = VersionResponse(true, "v0.5.6", true) + val versionResponse = VersionResponseV0("v0.5.6") 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) + coEvery { service.getToken(any(), any(), any()) } returns GetTokenResponseV0(TEST_TOKEN) assertThat(callAuthenticate()).isEqualTo(TEST_TOKEN) } @@ -76,7 +78,7 @@ class MealieDataSourceImplTest { val body = "{\"detail\":\"Unauthorized\"}".toJsonResponseBody() coEvery { service.getToken(any(), any(), any()) - } throws HttpException(Response.error(401, body)) + } throws HttpException(Response.error(401, body)) callAuthenticate() } @@ -85,7 +87,7 @@ class MealieDataSourceImplTest { val body = "{\"detail\":\"Something\"}".toJsonResponseBody() coEvery { service.getToken(any(), any(), any()) - } throws HttpException(Response.error(401, body)) + } throws HttpException(Response.error(401, body)) callAuthenticate() } @@ -94,7 +96,7 @@ class MealieDataSourceImplTest { val body = "".toJsonResponseBody() coEvery { service.getToken(any(), any(), any()) - } throws HttpException(Response.error(401, body)) + } throws HttpException(Response.error(401, body)) callAuthenticate() } @@ -112,6 +114,5 @@ class MealieDataSourceImplTest { 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