Merge pull request #76 from kirmanak/v1-support

Add V1 support
This commit is contained in:
Kirill Kamakin
2022-10-30 13:59:32 +01:00
committed by GitHub
106 changed files with 2682 additions and 1008 deletions

View File

@@ -1,7 +1,6 @@
package gq.kirmanak.mealient.data.add package gq.kirmanak.mealient.data.add
import gq.kirmanak.mealient.datasource.models.AddRecipeRequest
interface AddRecipeDataSource { interface AddRecipeDataSource {
suspend fun addRecipe(recipe: AddRecipeRequest): String
suspend fun addRecipe(recipe: AddRecipeInfo): String
} }

View File

@@ -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<AddRecipeIngredientInfo>,
val recipeInstructions: List<AddRecipeInstructionInfo>,
val settings: AddRecipeSettingsInfo,
)
data class AddRecipeSettingsInfo(
val disableComments: Boolean,
val public: Boolean,
)
data class AddRecipeIngredientInfo(
val note: String,
)
data class AddRecipeInstructionInfo(
val text: String,
)

View File

@@ -1,13 +1,12 @@
package gq.kirmanak.mealient.data.add package gq.kirmanak.mealient.data.add
import gq.kirmanak.mealient.datasource.models.AddRecipeRequest
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
interface AddRecipeRepo { interface AddRecipeRepo {
val addRecipeRequestFlow: Flow<AddRecipeRequest> val addRecipeRequestFlow: Flow<AddRecipeInfo>
suspend fun preserve(recipe: AddRecipeRequest) suspend fun preserve(recipe: AddRecipeInfo)
suspend fun clear() suspend fun clear()

View File

@@ -1,10 +1,10 @@
package gq.kirmanak.mealient.data.add.impl package gq.kirmanak.mealient.data.add.impl
import gq.kirmanak.mealient.data.add.AddRecipeDataSource 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.data.add.AddRecipeRepo
import gq.kirmanak.mealient.datasource.models.AddRecipeRequest
import gq.kirmanak.mealient.datastore.recipe.AddRecipeStorage 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.extensions.toDraft
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -20,10 +20,10 @@ class AddRecipeRepoImpl @Inject constructor(
private val logger: Logger, private val logger: Logger,
) : AddRecipeRepo { ) : AddRecipeRepo {
override val addRecipeRequestFlow: Flow<AddRecipeRequest> override val addRecipeRequestFlow: Flow<AddRecipeInfo>
get() = addRecipeStorage.updates.map { it.toAddRecipeRequest() } 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" } logger.v { "preserveRecipe() called with: recipe = $recipe" }
addRecipeStorage.save(recipe.toDraft()) addRecipeStorage.save(recipe.toDraft())
} }

View File

@@ -4,5 +4,5 @@ interface AuthDataSource {
/** /**
* Tries to acquire authentication token using the provided credentials * 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
} }

View File

@@ -1,15 +1,28 @@
package gq.kirmanak.mealient.data.auth.impl package gq.kirmanak.mealient.data.auth.impl
import gq.kirmanak.mealient.data.auth.AuthDataSource 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.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class AuthDataSourceImpl @Inject constructor( class AuthDataSourceImpl @Inject constructor(
private val mealieDataSource: MealieDataSource, private val serverInfoRepo: ServerInfoRepo,
private val v0Source: MealieDataSourceV0,
private val v1Source: MealieDataSourceV1,
) : AuthDataSource { ) : AuthDataSource {
override suspend fun authenticate(username: String, password: String, baseUrl: String): String = override suspend fun authenticate(
mealieDataSource.authenticate(baseUrl, username, password) 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)
}
}
} }

View File

@@ -3,8 +3,7 @@ package gq.kirmanak.mealient.data.auth.impl
import gq.kirmanak.mealient.data.auth.AuthDataSource import gq.kirmanak.mealient.data.auth.AuthDataSource
import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.auth.AuthStorage import gq.kirmanak.mealient.data.auth.AuthStorage
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
@@ -15,7 +14,6 @@ import javax.inject.Singleton
class AuthRepoImpl @Inject constructor( class AuthRepoImpl @Inject constructor(
private val authStorage: AuthStorage, private val authStorage: AuthStorage,
private val authDataSource: AuthDataSource, private val authDataSource: AuthDataSource,
private val baseURLStorage: BaseURLStorage,
private val logger: Logger, private val logger: Logger,
) : AuthRepo { ) : AuthRepo {
@@ -24,9 +22,9 @@ class AuthRepoImpl @Inject constructor(
override suspend fun authenticate(email: String, password: String) { override suspend fun authenticate(email: String, password: String) {
logger.v { "authenticate() called with: email = $email, password = $password" } logger.v { "authenticate() called with: email = $email, password = $password" }
authDataSource.authenticate(email, password, baseURLStorage.requireBaseURL()) val token = authDataSource.authenticate(email, password)
.let { AUTH_HEADER_FORMAT.format(it) } val header = AUTH_HEADER_FORMAT.format(token)
.let { authStorage.setAuthHeader(it) } authStorage.setAuthHeader(header)
authStorage.setEmail(email) authStorage.setEmail(email)
authStorage.setPassword(password) authStorage.setPassword(password)
} }

View File

@@ -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)
}

View File

@@ -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)
}

View File

@@ -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)
}
}

View File

@@ -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?
}

View File

@@ -0,0 +1,3 @@
package gq.kirmanak.mealient.data.baseurl
enum class ServerVersion { V0, V1 }

View File

@@ -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
}
}
}

View File

@@ -1,7 +1,5 @@
package gq.kirmanak.mealient.data.baseurl package gq.kirmanak.mealient.data.baseurl
data class VersionInfo( data class VersionInfo(
val production: Boolean,
val version: String, val version: String,
val demoStatus: Boolean,
) )

View File

@@ -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<String>
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))
}
}

View File

@@ -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<String>
get() = preferencesStorage.baseUrlKey
private val serverVersionKey: Preferences.Key<String>
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 <T> getValue(key: Preferences.Key<T>): T? = preferencesStorage.getValue(key)
}

View File

@@ -1,49 +1,80 @@
package gq.kirmanak.mealient.data.network package gq.kirmanak.mealient.data.network
import gq.kirmanak.mealient.data.add.AddRecipeDataSource 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.auth.AuthRepo
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.ServerVersion
import gq.kirmanak.mealient.data.baseurl.VersionInfo import gq.kirmanak.mealient.data.recipes.network.FullRecipeInfo
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.datasource.MealieDataSource import gq.kirmanak.mealient.data.recipes.network.RecipeSummaryInfo
import gq.kirmanak.mealient.datasource.models.AddRecipeRequest import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
import gq.kirmanak.mealient.datasource.models.NetworkError import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
import gq.kirmanak.mealient.extensions.toVersionInfo import gq.kirmanak.mealient.extensions.*
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@Singleton @Singleton
class MealieDataSourceWrapper @Inject constructor( class MealieDataSourceWrapper @Inject constructor(
private val baseURLStorage: BaseURLStorage, private val serverInfoRepo: ServerInfoRepo,
private val authRepo: AuthRepo, private val authRepo: AuthRepo,
private val mealieDataSource: MealieDataSource, private val v0Source: MealieDataSourceV0,
) : AddRecipeDataSource, RecipeDataSource, VersionDataSource { private val v1Source: MealieDataSourceV1,
) : AddRecipeDataSource, RecipeDataSource {
override suspend fun addRecipe(recipe: AddRecipeRequest): String = override suspend fun addRecipe(
withAuthHeader { token -> addRecipe(getUrl(), token, recipe) } 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 = override suspend fun requestRecipes(
mealieDataSource.getVersionInfo(baseUrl).toVersionInfo() start: Int,
limit: Int,
): List<RecipeSummaryInfo> = 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<GetRecipeSummaryResponse> = override suspend fun requestRecipeInfo(
withAuthHeader { token -> requestRecipes(getUrl(), token, start, limit) } 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 = private suspend inline fun <T> makeCall(block: (String?, String, ServerVersion) -> T): T {
withAuthHeader { token -> requestRecipeInfo(getUrl(), token, slug) } val authHeader = authRepo.getAuthHeader()
val url = serverInfoRepo.requireUrl()
private suspend fun getUrl() = baseURLStorage.requireBaseURL() val version = serverInfoRepo.getVersion()
return runCatchingExceptCancel { block(authHeader, url, version) }.getOrElse {
private suspend inline fun <T> withAuthHeader(block: MealieDataSource.(String?) -> T): T =
mealieDataSource.runCatching { block(authRepo.getAuthHeader()) }.getOrElse {
if (it is NetworkError.Unauthorized) { if (it is NetworkError.Unauthorized) {
authRepo.invalidateAuthHeader() authRepo.invalidateAuthHeader()
// Trying again with new authentication header // 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 { } else {
throw it throw it
} }
} }
}
} }

View File

@@ -1,7 +1,7 @@
package gq.kirmanak.mealient.data.recipes package gq.kirmanak.mealient.data.recipes
import androidx.paging.Pager 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 import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
interface RecipeRepo { interface RecipeRepo {
@@ -9,5 +9,5 @@ interface RecipeRepo {
suspend fun clearLocalData() suspend fun clearLocalData()
suspend fun loadRecipeInfo(recipeId: Long, recipeSlug: String): FullRecipeInfo suspend fun loadRecipeInfo(recipeId: String, recipeSlug: String): FullRecipeEntity
} }

View File

@@ -1,21 +1,21 @@
package gq.kirmanak.mealient.data.recipes.db package gq.kirmanak.mealient.data.recipes.db
import androidx.paging.PagingSource 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.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
interface RecipeStorage { interface RecipeStorage {
suspend fun saveRecipes(recipes: List<GetRecipeSummaryResponse>) suspend fun saveRecipes(recipes: List<RecipeSummaryInfo>)
fun queryRecipes(): PagingSource<Int, RecipeSummaryEntity> fun queryRecipes(): PagingSource<Int, RecipeSummaryEntity>
suspend fun refreshAll(recipes: List<GetRecipeSummaryResponse>) suspend fun refreshAll(recipes: List<RecipeSummaryInfo>)
suspend fun clearAllLocalData() 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
} }

View File

@@ -2,11 +2,12 @@ package gq.kirmanak.mealient.data.recipes.db
import androidx.paging.PagingSource import androidx.paging.PagingSource
import androidx.room.withTransaction 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.AppDb
import gq.kirmanak.mealient.database.recipe.RecipeDao import gq.kirmanak.mealient.database.recipe.RecipeDao
import gq.kirmanak.mealient.database.recipe.entity.* import gq.kirmanak.mealient.database.recipe.entity.FullRecipeEntity
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
import gq.kirmanak.mealient.extensions.recipeEntity import gq.kirmanak.mealient.extensions.recipeEntity
import gq.kirmanak.mealient.extensions.toRecipeEntity import gq.kirmanak.mealient.extensions.toRecipeEntity
import gq.kirmanak.mealient.extensions.toRecipeIngredientEntity import gq.kirmanak.mealient.extensions.toRecipeIngredientEntity
@@ -23,71 +24,14 @@ class RecipeStorageImpl @Inject constructor(
private val recipeDao: RecipeDao by lazy { db.recipeDao() } private val recipeDao: RecipeDao by lazy { db.recipeDao() }
override suspend fun saveRecipes( override suspend fun saveRecipes(
recipes: List<GetRecipeSummaryResponse> recipes: List<RecipeSummaryInfo>
) = db.withTransaction { ) = db.withTransaction {
logger.v { "saveRecipes() called with $recipes" } logger.v { "saveRecipes() called with $recipes" }
val tagEntities = mutableSetOf<TagEntity>()
tagEntities.addAll(recipeDao.queryAllTags())
val categoryEntities = mutableSetOf<CategoryEntity>()
categoryEntities.addAll(recipeDao.queryAllCategories())
val tagRecipeEntities = mutableSetOf<TagRecipeEntity>()
val categoryRecipeEntities = mutableSetOf<CategoryRecipeEntity>()
for (recipe in recipes) { for (recipe in recipes) {
val recipeSummaryEntity = recipe.recipeEntity() val recipeSummaryEntity = recipe.recipeEntity()
recipeDao.insertRecipe(recipeSummaryEntity) 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<CategoryEntity>,
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<TagEntity>,
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() return recipeDao.queryRecipesByPages()
} }
override suspend fun refreshAll(recipes: List<GetRecipeSummaryResponse>) { override suspend fun refreshAll(recipes: List<RecipeSummaryInfo>) {
logger.v { "refreshAll() called with: recipes = $recipes" } logger.v { "refreshAll() called with: recipes = $recipes" }
db.withTransaction { db.withTransaction {
recipeDao.removeAllRecipes() recipeDao.removeAllRecipes()
@@ -108,12 +52,10 @@ class RecipeStorageImpl @Inject constructor(
logger.v { "clearAllLocalData() called" } logger.v { "clearAllLocalData() called" }
db.withTransaction { db.withTransaction {
recipeDao.removeAllRecipes() 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" } logger.v { "saveRecipeInfo() called with: recipe = $recipe" }
db.withTransaction { db.withTransaction {
recipeDao.insertRecipe(recipe.toRecipeEntity()) 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" } logger.v { "queryRecipeInfo() called with: recipeId = $recipeId" }
val fullRecipeInfo = checkNotNull(recipeDao.queryFullRecipeInfo(recipeId)) { val fullRecipeInfo = checkNotNull(recipeDao.queryFullRecipeInfo(recipeId)) {
"Can't find recipe by id $recipeId in DB" "Can't find recipe by id $recipeId in DB"

View File

@@ -1,6 +1,6 @@
package gq.kirmanak.mealient.data.recipes.impl 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 gq.kirmanak.mealient.logging.Logger
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import javax.inject.Inject import javax.inject.Inject
@@ -8,7 +8,7 @@ import javax.inject.Singleton
@Singleton @Singleton
class RecipeImageUrlProviderImpl @Inject constructor( class RecipeImageUrlProviderImpl @Inject constructor(
private val baseURLStorage: BaseURLStorage, private val serverInfoRepo: ServerInfoRepo,
private val logger: Logger, private val logger: Logger,
) : RecipeImageUrlProvider { ) : RecipeImageUrlProvider {
@@ -16,7 +16,7 @@ class RecipeImageUrlProviderImpl @Inject constructor(
logger.v { "generateImageUrl() called with: slug = $slug" } logger.v { "generateImageUrl() called with: slug = $slug" }
slug?.takeUnless { it.isBlank() } ?: return null slug?.takeUnless { it.isBlank() } ?: return null
val imagePath = IMAGE_PATH_FORMAT.format(slug) 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() val result = baseUrl?.toHttpUrlOrNull()
?.newBuilder() ?.newBuilder()
?.addPathSegments(imagePath) ?.addPathSegments(imagePath)

View File

@@ -7,9 +7,9 @@ import androidx.paging.PagingConfig
import gq.kirmanak.mealient.data.recipes.RecipeRepo import gq.kirmanak.mealient.data.recipes.RecipeRepo
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource 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.database.recipe.entity.RecipeSummaryEntity
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton
@@ -38,7 +38,7 @@ class RecipeRepoImpl @Inject constructor(
storage.clearAllLocalData() 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" } logger.v { "loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug" }
runCatchingExceptCancel { runCatchingExceptCancel {

View File

@@ -7,7 +7,7 @@ import androidx.paging.LoadType.REFRESH
import gq.kirmanak.mealient.data.recipes.db.RecipeStorage import gq.kirmanak.mealient.data.recipes.db.RecipeStorage
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity 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 gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject import javax.inject.Inject
import javax.inject.Singleton import javax.inject.Singleton

View File

@@ -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<RecipeIngredientInfo>,
val recipeInstructions: List<RecipeInstructionInfo>,
)
data class RecipeIngredientInfo(
val note: String,
)
data class RecipeInstructionInfo(
val text: String,
)

View File

@@ -1,10 +1,7 @@
package gq.kirmanak.mealient.data.recipes.network package gq.kirmanak.mealient.data.recipes.network
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
interface RecipeDataSource { interface RecipeDataSource {
suspend fun requestRecipes(start: Int, limit: Int): List<GetRecipeSummaryResponse> suspend fun requestRecipes(start: Int, limit: Int): List<RecipeSummaryInfo>
suspend fun requestRecipeInfo(slug: String): GetRecipeResponse suspend fun requestRecipeInfo(slug: String): FullRecipeInfo
} }

View File

@@ -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
)

View File

@@ -7,6 +7,8 @@ interface PreferencesStorage {
val baseUrlKey: Preferences.Key<String> val baseUrlKey: Preferences.Key<String>
val serverVersionKey: Preferences.Key<String>
val isDisclaimerAcceptedKey: Preferences.Key<Boolean> val isDisclaimerAcceptedKey: Preferences.Key<Boolean>
suspend fun <T> getValue(key: Preferences.Key<T>): T? suspend fun <T> getValue(key: Preferences.Key<T>): T?

View File

@@ -18,6 +18,8 @@ class PreferencesStorageImpl @Inject constructor(
override val baseUrlKey = stringPreferencesKey("baseUrl") override val baseUrlKey = stringPreferencesKey("baseUrl")
override val serverVersionKey = stringPreferencesKey("serverVersion")
override val isDisclaimerAcceptedKey = booleanPreferencesKey("isDisclaimedAccepted") override val isDisclaimerAcceptedKey = booleanPreferencesKey("isDisclaimedAccepted")
override suspend fun <T> getValue(key: Preferences.Key<T>): T? { override suspend fun <T> getValue(key: Preferences.Key<T>): T? {

View File

@@ -4,10 +4,8 @@ import dagger.Binds
import dagger.Module import dagger.Module
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent import dagger.hilt.components.SingletonComponent
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.*
import gq.kirmanak.mealient.data.baseurl.VersionDataSource import gq.kirmanak.mealient.data.baseurl.impl.ServerInfoStorageImpl
import gq.kirmanak.mealient.data.baseurl.impl.BaseURLStorageImpl
import gq.kirmanak.mealient.data.network.MealieDataSourceWrapper
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@@ -16,9 +14,13 @@ interface BaseURLModule {
@Binds @Binds
@Singleton @Singleton
fun bindVersionDataSource(mealieDataSourceWrapper: MealieDataSourceWrapper): VersionDataSource fun bindVersionDataSource(versionDataSourceImpl: VersionDataSourceImpl): VersionDataSource
@Binds @Binds
@Singleton @Singleton
fun bindBaseUrlStorage(baseURLStorageImpl: BaseURLStorageImpl): BaseURLStorage fun bindBaseUrlStorage(baseURLStorageImpl: ServerInfoStorageImpl): ServerInfoStorage
@Binds
@Singleton
fun bindServerInfoRepo(serverInfoRepoImpl: ServerInfoRepoImpl): ServerInfoRepo
} }

View File

@@ -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 <T> runCatchingExceptCancel(block: () -> T): Result<T> = try {
Result.success(block())
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
Result.failure(e)
}

View File

@@ -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(),
)

View File

@@ -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,
)

View File

@@ -12,12 +12,12 @@ import androidx.fragment.app.viewModels
import by.kirich1409.viewbindingdelegate.viewBinding import by.kirich1409.viewbindingdelegate.viewBinding
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.R 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.FragmentAddRecipeBinding
import gq.kirmanak.mealient.databinding.ViewSingleInputBinding 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.checkIfInputIsEmpty
import gq.kirmanak.mealient.extensions.collectWhenViewResumed import gq.kirmanak.mealient.extensions.collectWhenViewResumed
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
@@ -122,14 +122,15 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
private fun saveValues() = with(binding) { private fun saveValues() = with(binding) {
logger.v { "saveValues() called" } logger.v { "saveValues() called" }
val instructions = parseInputRows(instructionsFlow).map { AddRecipeInstruction(text = it) } val instructions =
val ingredients = parseInputRows(ingredientsFlow).map { AddRecipeIngredient(note = it) } parseInputRows(instructionsFlow).map { AddRecipeInstructionInfo(text = it) }
val settings = AddRecipeSettings( val ingredients = parseInputRows(ingredientsFlow).map { AddRecipeIngredientInfo(note = it) }
val settings = AddRecipeSettingsInfo(
public = publicRecipe.isChecked, public = publicRecipe.isChecked,
disableComments = disableComments.isChecked, disableComments = disableComments.isChecked,
) )
viewModel.preserve( viewModel.preserve(
AddRecipeRequest( AddRecipeInfo(
name = recipeNameInput.text.toString(), name = recipeNameInput.text.toString(),
description = recipeDescriptionInput.text.toString(), description = recipeDescriptionInput.text.toString(),
recipeYield = recipeYieldInput.text.toString(), recipeYield = recipeYieldInput.text.toString(),
@@ -148,7 +149,7 @@ class AddRecipeFragment : Fragment(R.layout.fragment_add_recipe) {
.filterNot { it.isBlank() } .filterNot { it.isBlank() }
.toList() .toList()
private fun onSavedInputLoaded(request: AddRecipeRequest) = with(binding) { private fun onSavedInputLoaded(request: AddRecipeInfo) = with(binding) {
logger.v { "onSavedInputLoaded() called with: request = $request" } logger.v { "onSavedInputLoaded() called with: request = $request" }
recipeNameInput.setText(request.name) recipeNameInput.setText(request.name)
recipeDescriptionInput.setText(request.description) recipeDescriptionInput.setText(request.description)

View File

@@ -3,9 +3,9 @@ package gq.kirmanak.mealient.ui.add
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.add.AddRecipeInfo
import gq.kirmanak.mealient.data.add.AddRecipeRepo import gq.kirmanak.mealient.data.add.AddRecipeRepo
import gq.kirmanak.mealient.datasource.models.AddRecipeRequest import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.channels.Channel import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@@ -23,8 +23,8 @@ class AddRecipeViewModel @Inject constructor(
private val _addRecipeResultChannel = Channel<Boolean>(Channel.UNLIMITED) private val _addRecipeResultChannel = Channel<Boolean>(Channel.UNLIMITED)
val addRecipeResult: Flow<Boolean> get() = _addRecipeResultChannel.receiveAsFlow() val addRecipeResult: Flow<Boolean> get() = _addRecipeResultChannel.receiveAsFlow()
private val _preservedAddRecipeRequestChannel = Channel<AddRecipeRequest>(Channel.UNLIMITED) private val _preservedAddRecipeRequestChannel = Channel<AddRecipeInfo>(Channel.UNLIMITED)
val preservedAddRecipeRequest: Flow<AddRecipeRequest> val preservedAddRecipeRequest: Flow<AddRecipeInfo>
get() = _preservedAddRecipeRequestChannel.receiveAsFlow() get() = _preservedAddRecipeRequestChannel.receiveAsFlow()
fun loadPreservedRequest() { 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" } logger.v { "preserve() called with: request = $request" }
viewModelScope.launch { addRecipeRepo.preserve(request) } viewModelScope.launch { addRecipeRepo.preserve(request) }
} }

View File

@@ -10,7 +10,7 @@ import by.kirich1409.viewbindingdelegate.viewBinding
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.R import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.databinding.FragmentAuthenticationBinding 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.extensions.checkIfInputIsEmpty
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState

View File

@@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.auth.AuthRepo 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.logging.Logger
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState
import kotlinx.coroutines.launch import kotlinx.coroutines.launch

View File

@@ -10,7 +10,7 @@ import by.kirich1409.viewbindingdelegate.viewBinding
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import gq.kirmanak.mealient.R import gq.kirmanak.mealient.R
import gq.kirmanak.mealient.databinding.FragmentBaseUrlBinding 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.extensions.checkIfInputIsEmpty
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState

View File

@@ -5,9 +5,9 @@ import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel 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.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.logging.Logger
import gq.kirmanak.mealient.ui.OperationUiState import gq.kirmanak.mealient.ui.OperationUiState
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -15,7 +15,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class BaseURLViewModel @Inject constructor( class BaseURLViewModel @Inject constructor(
private val baseURLStorage: BaseURLStorage, private val serverInfoRepo: ServerInfoRepo,
private val versionDataSource: VersionDataSource, private val versionDataSource: VersionDataSource,
private val logger: Logger, private val logger: Logger,
) : ViewModel() { ) : ViewModel() {
@@ -35,8 +35,8 @@ class BaseURLViewModel @Inject constructor(
logger.v { "checkBaseURL() called with: baseURL = $baseURL" } logger.v { "checkBaseURL() called with: baseURL = $baseURL" }
val result = runCatchingExceptCancel { val result = runCatchingExceptCancel {
// If it returns proper version info then it must be a Mealie // If it returns proper version info then it must be a Mealie
versionDataSource.getVersionInfo(baseURL) val version = versionDataSource.getVersionInfo(baseURL).version
baseURLStorage.storeBaseURL(baseURL) serverInfoRepo.storeBaseURL(baseURL, version)
} }
logger.i { "checkBaseURL: result is $result" } logger.i { "checkBaseURL: result is $result" }
_uiState.value = OperationUiState.fromResult(result) _uiState.value = OperationUiState.fromResult(result)

View File

@@ -44,7 +44,7 @@ class RecipeModelLoader private constructor(
options: Options? options: Options?
): String? { ): String? {
logger.v { "getUrl() called with: model = $model, width = $width, height = $height, options = $options" } 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( override fun getHeaders(

View File

@@ -1,9 +1,9 @@
package gq.kirmanak.mealient.ui.recipes.info 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( data class RecipeInfoUiState(
val areIngredientsVisible: Boolean = false, val areIngredientsVisible: Boolean = false,
val areInstructionsVisible: Boolean = false, val areInstructionsVisible: Boolean = false,
val recipeInfo: FullRecipeInfo? = null, val recipeInfo: FullRecipeEntity? = null,
) )

View File

@@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel import dagger.hilt.android.lifecycle.HiltViewModel
import gq.kirmanak.mealient.data.recipes.RecipeRepo 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 gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import javax.inject.Inject import javax.inject.Inject
@@ -20,7 +20,7 @@ class RecipeInfoViewModel @Inject constructor(
private val _uiState = MutableLiveData(RecipeInfoUiState()) private val _uiState = MutableLiveData(RecipeInfoUiState())
val uiState: LiveData<RecipeInfoUiState> get() = _uiState val uiState: LiveData<RecipeInfoUiState> get() = _uiState
fun loadRecipeInfo(recipeId: Long, recipeSlug: String) { fun loadRecipeInfo(recipeId: String, recipeSlug: String) {
logger.v { "loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug" } logger.v { "loadRecipeInfo() called with: recipeId = $recipeId, recipeSlug = $recipeSlug" }
_uiState.value = RecipeInfoUiState() _uiState.value = RecipeInfoUiState()
viewModelScope.launch { viewModelScope.launch {

View File

@@ -6,7 +6,7 @@ import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope import androidx.lifecycle.viewModelScope
import androidx.navigation.NavDirections import androidx.navigation.NavDirections
import dagger.hilt.android.lifecycle.HiltViewModel 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 gq.kirmanak.mealient.data.disclaimer.DisclaimerStorage
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@@ -15,7 +15,7 @@ import javax.inject.Inject
@HiltViewModel @HiltViewModel
class SplashViewModel @Inject constructor( class SplashViewModel @Inject constructor(
private val disclaimerStorage: DisclaimerStorage, private val disclaimerStorage: DisclaimerStorage,
private val baseURLStorage: BaseURLStorage, private val serverInfoRepo: ServerInfoRepo,
) : ViewModel() { ) : ViewModel() {
private val _nextDestination = MutableLiveData<NavDirections>() private val _nextDestination = MutableLiveData<NavDirections>()
val nextDestination: LiveData<NavDirections> = _nextDestination val nextDestination: LiveData<NavDirections> = _nextDestination
@@ -25,7 +25,7 @@ class SplashViewModel @Inject constructor(
delay(1000) delay(1000)
_nextDestination.value = when { _nextDestination.value = when {
!disclaimerStorage.isDisclaimerAccepted() -> SplashFragmentDirections.actionSplashFragmentToDisclaimerFragment() !disclaimerStorage.isDisclaimerAccepted() -> SplashFragmentDirections.actionSplashFragmentToDisclaimerFragment()
baseURLStorage.getBaseURL() == null -> SplashFragmentDirections.actionSplashFragmentToBaseURLFragment() serverInfoRepo.getUrl() == null -> SplashFragmentDirections.actionSplashFragmentToBaseURLFragment()
else -> SplashFragmentDirections.actionSplashFragmentToRecipesFragment() else -> SplashFragmentDirections.actionSplashFragmentToRecipesFragment()
} }
} }

View File

@@ -36,7 +36,7 @@
app:argType="string" /> app:argType="string" />
<argument <argument
android:name="recipe_id" android:name="recipe_id"
app:argType="long" /> app:argType="string" />
</dialog> </dialog>
<fragment <fragment
android:id="@+id/disclaimerFragment" android:id="@+id/disclaimerFragment"

View File

@@ -4,11 +4,13 @@ import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.data.auth.AuthDataSource import gq.kirmanak.mealient.data.auth.AuthDataSource
import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.auth.AuthStorage import gq.kirmanak.mealient.data.auth.AuthStorage
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD import gq.kirmanak.mealient.test.AuthImplTestData.TEST_PASSWORD
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_SERVER_VERSION
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN import gq.kirmanak.mealient.test.AuthImplTestData.TEST_TOKEN
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_USERNAME import gq.kirmanak.mealient.test.AuthImplTestData.TEST_USERNAME
import io.mockk.* import io.mockk.*
@@ -27,7 +29,7 @@ class AuthRepoImplTest {
lateinit var dataSource: AuthDataSource lateinit var dataSource: AuthDataSource
@MockK @MockK
lateinit var baseURLStorage: BaseURLStorage lateinit var serverInfoRepo: ServerInfoRepo
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
lateinit var storage: AuthStorage lateinit var storage: AuthStorage
@@ -40,7 +42,7 @@ class AuthRepoImplTest {
@Before @Before
fun setUp() { fun setUp() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
subject = AuthRepoImpl(storage, dataSource, baseURLStorage, logger) subject = AuthRepoImpl(storage, dataSource, logger)
} }
@Test @Test
@@ -51,14 +53,9 @@ class AuthRepoImplTest {
@Test @Test
fun `when authenticate successfully then saves to storage`() = runTest { fun `when authenticate successfully then saves to storage`() = runTest {
coEvery { coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION
dataSource.authenticate( coEvery { dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD)) } returns TEST_TOKEN
eq(TEST_USERNAME), coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
eq(TEST_PASSWORD),
eq(TEST_BASE_URL)
)
} returns TEST_TOKEN
coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL
subject.authenticate(TEST_USERNAME, TEST_PASSWORD) subject.authenticate(TEST_USERNAME, TEST_PASSWORD)
coVerifyAll { coVerifyAll {
storage.setAuthHeader(TEST_AUTH_HEADER) storage.setAuthHeader(TEST_AUTH_HEADER)
@@ -70,9 +67,9 @@ class AuthRepoImplTest {
@Test @Test
fun `when authenticate fails then does not change storage`() = runTest { fun `when authenticate fails then does not change storage`() = runTest {
coEvery { dataSource.authenticate(any(), any(), any()) } throws RuntimeException() coEvery { dataSource.authenticate(any(), any()) } throws RuntimeException()
coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
runCatching { subject.authenticate("invalid", "") } runCatchingExceptCancel { subject.authenticate("invalid", "") }
confirmVerified(storage) confirmVerified(storage)
} }
@@ -107,22 +104,19 @@ class AuthRepoImplTest {
fun `when invalidate with credentials then calls authenticate`() = runTest { fun `when invalidate with credentials then calls authenticate`() = runTest {
coEvery { storage.getEmail() } returns TEST_USERNAME coEvery { storage.getEmail() } returns TEST_USERNAME
coEvery { storage.getPassword() } returns TEST_PASSWORD coEvery { storage.getPassword() } returns TEST_PASSWORD
coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION
coEvery { coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD), eq(TEST_BASE_URL)) coEvery { dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD)) } returns TEST_TOKEN
} returns TEST_TOKEN
subject.invalidateAuthHeader() subject.invalidateAuthHeader()
coVerifyAll { coVerifyAll { dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD)) }
dataSource.authenticate(eq(TEST_USERNAME), eq(TEST_PASSWORD), eq(TEST_BASE_URL))
}
} }
@Test @Test
fun `when invalidate with credentials and auth fails then clears email`() = runTest { fun `when invalidate with credentials and auth fails then clears email`() = runTest {
coEvery { storage.getEmail() } returns "invalid" coEvery { storage.getEmail() } returns "invalid"
coEvery { storage.getPassword() } returns "" coEvery { storage.getPassword() } returns ""
coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
coEvery { dataSource.authenticate(any(), any(), any()) } throws RuntimeException() coEvery { dataSource.authenticate(any(), any()) } throws RuntimeException()
subject.invalidateAuthHeader() subject.invalidateAuthHeader()
coVerify { storage.setEmail(null) } coVerify { storage.setEmail(null) }
} }

View File

@@ -2,7 +2,7 @@ package gq.kirmanak.mealient.data.baseurl
import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.data.baseurl.impl.BaseURLStorageImpl import gq.kirmanak.mealient.data.baseurl.impl.ServerInfoStorageImpl
import gq.kirmanak.mealient.data.storage.PreferencesStorage import gq.kirmanak.mealient.data.storage.PreferencesStorage
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.coEvery import io.mockk.coEvery
@@ -15,20 +15,22 @@ import org.junit.Before
import org.junit.Test import org.junit.Test
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class BaseURLStorageImplTest { class ServerInfoStorageImplTest {
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
lateinit var preferencesStorage: PreferencesStorage lateinit var preferencesStorage: PreferencesStorage
lateinit var subject: BaseURLStorage lateinit var subject: ServerInfoStorage
private val baseUrlKey = stringPreferencesKey("baseUrlKey") private val baseUrlKey = stringPreferencesKey("baseUrlKey")
private val serverVersionKey = stringPreferencesKey("serverVersionKey")
@Before @Before
fun setUp() { fun setUp() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
subject = BaseURLStorageImpl(preferencesStorage) subject = ServerInfoStorageImpl(preferencesStorage)
every { preferencesStorage.baseUrlKey } returns baseUrlKey every { preferencesStorage.baseUrlKey } returns baseUrlKey
every { preferencesStorage.serverVersionKey } returns serverVersionKey
} }
@Test @Test
@@ -37,30 +39,21 @@ class BaseURLStorageImplTest {
assertThat(subject.getBaseURL()).isNull() assertThat(subject.getBaseURL()).isNull()
} }
@Test(expected = IllegalStateException::class)
fun `when requireBaseURL and preferences storage empty then IllegalStateException`() = runTest {
coEvery { preferencesStorage.getValue(eq(baseUrlKey)) } returns null
subject.requireBaseURL()
}
@Test @Test
fun `when getBaseUrl and preferences storage has value then value`() = runTest { fun `when getBaseUrl and preferences storage has value then value`() = runTest {
coEvery { preferencesStorage.getValue(eq(baseUrlKey)) } returns "baseUrl" coEvery { preferencesStorage.getValue(eq(baseUrlKey)) } returns "baseUrl"
assertThat(subject.getBaseURL()).isEqualTo("baseUrl") assertThat(subject.getBaseURL()).isEqualTo("baseUrl")
} }
@Test
fun `when requireBaseURL and preferences storage has value then value`() = runTest {
coEvery { preferencesStorage.getValue(eq(baseUrlKey)) } returns "baseUrl"
assertThat(subject.requireBaseURL()).isEqualTo("baseUrl")
}
@Test @Test
fun `when storeBaseURL then calls preferences storage`() = runTest { fun `when storeBaseURL then calls preferences storage`() = runTest {
subject.storeBaseURL("baseUrl") subject.storeBaseURL("baseUrl", "v0.5.6")
coVerify { coVerify {
preferencesStorage.baseUrlKey preferencesStorage.baseUrlKey
preferencesStorage.storeValues(eq(Pair(baseUrlKey, "baseUrl"))) preferencesStorage.storeValues(
eq(Pair(baseUrlKey, "baseUrl")),
eq(Pair(serverVersionKey, "v0.5.6")),
)
} }
} }
} }

View File

@@ -1,16 +1,19 @@
package gq.kirmanak.mealient.data.network package gq.kirmanak.mealient.data.network
import gq.kirmanak.mealient.data.auth.AuthRepo import gq.kirmanak.mealient.data.auth.AuthRepo
import gq.kirmanak.mealient.data.baseurl.BaseURLStorage import gq.kirmanak.mealient.data.baseurl.ServerInfoRepo
import gq.kirmanak.mealient.datasource.MealieDataSource import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.datasource.models.NetworkError import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER import gq.kirmanak.mealient.test.AuthImplTestData.TEST_AUTH_HEADER
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
import gq.kirmanak.mealient.test.RecipeImplTestData.GET_CAKE_RESPONSE import gq.kirmanak.mealient.test.AuthImplTestData.TEST_SERVER_VERSION
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerifyAll import io.mockk.coVerifyAll
import io.mockk.impl.annotations.MockK import io.mockk.impl.annotations.MockK
import io.mockk.mockk
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.runTest
import org.junit.Before import org.junit.Before
@@ -21,32 +24,37 @@ import java.io.IOException
class MealieDataSourceWrapperTest { class MealieDataSourceWrapperTest {
@MockK @MockK
lateinit var baseURLStorage: BaseURLStorage lateinit var serverInfoRepo: ServerInfoRepo
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
lateinit var authRepo: AuthRepo lateinit var authRepo: AuthRepo
@MockK @MockK
lateinit var mealieDataSource: MealieDataSource lateinit var v0Source: MealieDataSourceV0
@MockK
lateinit var v1Source: MealieDataSourceV1
lateinit var subject: MealieDataSourceWrapper lateinit var subject: MealieDataSourceWrapper
@Before @Before
fun setUp() { fun setUp() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
subject = MealieDataSourceWrapper(baseURLStorage, authRepo, mealieDataSource) subject = MealieDataSourceWrapper(serverInfoRepo, authRepo, v0Source, v1Source)
} }
@Test @Test
fun `when withAuthHeader fails with Unauthorized then invalidates auth`() = runTest { fun `when withAuthHeader fails with Unauthorized then invalidates auth`() = runTest {
coEvery { baseURLStorage.requireBaseURL() } returns TEST_BASE_URL coEvery { serverInfoRepo.getVersion() } returns TEST_SERVER_VERSION
coEvery { serverInfoRepo.requireUrl() } returns TEST_BASE_URL
coEvery { authRepo.getAuthHeader() } returns null andThen TEST_AUTH_HEADER coEvery { authRepo.getAuthHeader() } returns null andThen TEST_AUTH_HEADER
coEvery { coEvery {
mealieDataSource.requestRecipeInfo(eq(TEST_BASE_URL), isNull(), eq("cake")) v0Source.requestRecipeInfo(eq(TEST_BASE_URL), isNull(), eq("cake"))
} throws NetworkError.Unauthorized(IOException()) } throws NetworkError.Unauthorized(IOException())
val successResponse = mockk<GetRecipeResponseV0>(relaxed = true)
coEvery { coEvery {
mealieDataSource.requestRecipeInfo(eq(TEST_BASE_URL), eq(TEST_AUTH_HEADER), eq("cake")) v0Source.requestRecipeInfo(eq(TEST_BASE_URL), eq(TEST_AUTH_HEADER), eq("cake"))
} returns GET_CAKE_RESPONSE } returns successResponse
subject.requestRecipeInfo("cake") subject.requestRecipeInfo("cake")
coVerifyAll { coVerifyAll {
authRepo.getAuthHeader() authRepo.getAuthHeader()

View File

@@ -3,10 +3,6 @@ package gq.kirmanak.mealient.data.recipes.db
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import dagger.hilt.android.testing.HiltAndroidTest import dagger.hilt.android.testing.HiltAndroidTest
import gq.kirmanak.mealient.database.AppDb 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.HiltRobolectricTest
import gq.kirmanak.mealient.test.RecipeImplTestData.BREAD_INGREDIENT import gq.kirmanak.mealient.test.RecipeImplTestData.BREAD_INGREDIENT
import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_BREAD_RECIPE_INGREDIENT_ENTITY import gq.kirmanak.mealient.test.RecipeImplTestData.CAKE_BREAD_RECIPE_INGREDIENT_ENTITY
@@ -36,28 +32,6 @@ class RecipeStorageImplTest : HiltRobolectricTest() {
@Inject @Inject
lateinit var appDb: AppDb 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 @Test
fun `when saveRecipes then saves recipes`() = runTest { fun `when saveRecipes then saves recipes`() = runTest {
subject.saveRecipes(TEST_RECIPE_SUMMARIES) 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 @Test
fun `when refreshAll then old recipes aren't preserved`() = runTest { fun `when refreshAll then old recipes aren't preserved`() = runTest {
subject.saveRecipes(TEST_RECIPE_SUMMARIES) subject.saveRecipes(TEST_RECIPE_SUMMARIES)
@@ -100,28 +50,6 @@ class RecipeStorageImplTest : HiltRobolectricTest() {
assertThat(actual).containsExactly(CAKE_RECIPE_SUMMARY_ENTITY) 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 @Test
fun `when clearAllLocalData then recipes aren't preserved`() = runTest { fun `when clearAllLocalData then recipes aren't preserved`() = runTest {
subject.saveRecipes(TEST_RECIPE_SUMMARIES) subject.saveRecipes(TEST_RECIPE_SUMMARIES)
@@ -130,27 +58,11 @@ class RecipeStorageImplTest : HiltRobolectricTest() {
assertThat(actual).isEmpty() 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 @Test
fun `when saveRecipeInfo then saves recipe info`() = runTest { fun `when saveRecipeInfo then saves recipe info`() = runTest {
subject.saveRecipes(listOf(RECIPE_SUMMARY_CAKE)) subject.saveRecipes(listOf(RECIPE_SUMMARY_CAKE))
subject.saveRecipeInfo(GET_CAKE_RESPONSE) subject.saveRecipeInfo(GET_CAKE_RESPONSE)
val actual = appDb.recipeDao().queryFullRecipeInfo(1) val actual = appDb.recipeDao().queryFullRecipeInfo("1")
assertThat(actual).isEqualTo(FULL_CAKE_INFO_ENTITY) assertThat(actual).isEqualTo(FULL_CAKE_INFO_ENTITY)
} }
@@ -159,7 +71,7 @@ class RecipeStorageImplTest : HiltRobolectricTest() {
subject.saveRecipes(listOf(RECIPE_SUMMARY_CAKE, RECIPE_SUMMARY_PORRIDGE)) subject.saveRecipes(listOf(RECIPE_SUMMARY_CAKE, RECIPE_SUMMARY_PORRIDGE))
subject.saveRecipeInfo(GET_CAKE_RESPONSE) subject.saveRecipeInfo(GET_CAKE_RESPONSE)
subject.saveRecipeInfo(GET_PORRIDGE_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) assertThat(actual).isEqualTo(FULL_PORRIDGE_INFO_ENTITY)
} }
@@ -169,7 +81,7 @@ class RecipeStorageImplTest : HiltRobolectricTest() {
subject.saveRecipeInfo(GET_CAKE_RESPONSE) subject.saveRecipeInfo(GET_CAKE_RESPONSE)
val newRecipe = GET_CAKE_RESPONSE.copy(recipeIngredients = listOf(BREAD_INGREDIENT)) val newRecipe = GET_CAKE_RESPONSE.copy(recipeIngredients = listOf(BREAD_INGREDIENT))
subject.saveRecipeInfo(newRecipe) 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)) val expected = listOf(CAKE_BREAD_RECIPE_INGREDIENT_ENTITY.copy(localId = 3))
assertThat(actual).isEqualTo(expected) assertThat(actual).isEqualTo(expected)
} }
@@ -180,7 +92,7 @@ class RecipeStorageImplTest : HiltRobolectricTest() {
subject.saveRecipeInfo(GET_CAKE_RESPONSE) subject.saveRecipeInfo(GET_CAKE_RESPONSE)
val newRecipe = GET_CAKE_RESPONSE.copy(recipeInstructions = listOf(MIX_INSTRUCTION)) val newRecipe = GET_CAKE_RESPONSE.copy(recipeInstructions = listOf(MIX_INSTRUCTION))
subject.saveRecipeInfo(newRecipe) 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)) val expected = listOf(MIX_CAKE_RECIPE_INSTRUCTION_ENTITY.copy(localId = 3))
assertThat(actual).isEqualTo(expected) assertThat(actual).isEqualTo(expected)
} }

View File

@@ -1,7 +1,7 @@
package gq.kirmanak.mealient.data.recipes.impl package gq.kirmanak.mealient.data.recipes.impl
import com.google.common.truth.Truth.assertThat 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 gq.kirmanak.mealient.logging.Logger
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.coEvery import io.mockk.coEvery
@@ -17,7 +17,7 @@ class RecipeImageUrlProviderImplTest {
lateinit var subject: RecipeImageUrlProvider lateinit var subject: RecipeImageUrlProvider
@MockK @MockK
lateinit var baseURLStorage: BaseURLStorage lateinit var serverInfoRepo: ServerInfoRepo
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
lateinit var logger: Logger lateinit var logger: Logger
@@ -25,7 +25,7 @@ class RecipeImageUrlProviderImplTest {
@Before @Before
fun setUp() { fun setUp() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
subject = RecipeImageUrlProviderImpl(baseURLStorage, logger) subject = RecipeImageUrlProviderImpl(serverInfoRepo, logger)
prepareBaseURL("https://google.com/") prepareBaseURL("https://google.com/")
} }
@@ -81,6 +81,6 @@ class RecipeImageUrlProviderImplTest {
} }
private fun prepareBaseURL(baseURL: String?) { private fun prepareBaseURL(baseURL: String?) {
coEvery { baseURLStorage.getBaseURL() } returns baseURL coEvery { serverInfoRepo.getUrl() } returns baseURL
} }
} }

View File

@@ -47,24 +47,24 @@ class RecipeRepoImplTest {
@Test @Test
fun `when loadRecipeInfo then loads recipe`() = runTest { fun `when loadRecipeInfo then loads recipe`() = runTest {
coEvery { dataSource.requestRecipeInfo(eq("cake")) } returns GET_CAKE_RESPONSE coEvery { dataSource.requestRecipeInfo(eq("cake")) } returns GET_CAKE_RESPONSE
coEvery { storage.queryRecipeInfo(eq(1)) } returns FULL_CAKE_INFO_ENTITY coEvery { storage.queryRecipeInfo(eq("1")) } returns FULL_CAKE_INFO_ENTITY
val actual = subject.loadRecipeInfo(1, "cake") val actual = subject.loadRecipeInfo("1", "cake")
assertThat(actual).isEqualTo(FULL_CAKE_INFO_ENTITY) assertThat(actual).isEqualTo(FULL_CAKE_INFO_ENTITY)
} }
@Test @Test
fun `when loadRecipeInfo then saves to DB`() = runTest { fun `when loadRecipeInfo then saves to DB`() = runTest {
coEvery { dataSource.requestRecipeInfo(eq("cake")) } returns GET_CAKE_RESPONSE coEvery { dataSource.requestRecipeInfo(eq("cake")) } returns GET_CAKE_RESPONSE
coEvery { storage.queryRecipeInfo(eq(1)) } returns FULL_CAKE_INFO_ENTITY coEvery { storage.queryRecipeInfo(eq("1")) } returns FULL_CAKE_INFO_ENTITY
subject.loadRecipeInfo(1, "cake") subject.loadRecipeInfo("1", "cake")
coVerify { storage.saveRecipeInfo(eq(GET_CAKE_RESPONSE)) } coVerify { storage.saveRecipeInfo(eq(GET_CAKE_RESPONSE)) }
} }
@Test @Test
fun `when loadRecipeInfo with error then loads from DB`() = runTest { fun `when loadRecipeInfo with error then loads from DB`() = runTest {
coEvery { dataSource.requestRecipeInfo(eq("cake")) } throws RuntimeException() coEvery { dataSource.requestRecipeInfo(eq("cake")) } throws RuntimeException()
coEvery { storage.queryRecipeInfo(eq(1)) } returns FULL_CAKE_INFO_ENTITY coEvery { storage.queryRecipeInfo(eq("1")) } returns FULL_CAKE_INFO_ENTITY
val actual = subject.loadRecipeInfo(1, "cake") val actual = subject.loadRecipeInfo("1", "cake")
assertThat(actual).isEqualTo(FULL_CAKE_INFO_ENTITY) assertThat(actual).isEqualTo(FULL_CAKE_INFO_ENTITY)
} }
} }

View File

@@ -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.db.RecipeStorage
import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource
import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity 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.logging.Logger
import gq.kirmanak.mealient.test.RecipeImplTestData.TEST_RECIPE_SUMMARIES import gq.kirmanak.mealient.test.RecipeImplTestData.TEST_RECIPE_SUMMARIES
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations

View File

@@ -1,10 +1,10 @@
package gq.kirmanak.mealient.extensions package gq.kirmanak.mealient.extensions
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.datasource.models.AddRecipeIngredient import gq.kirmanak.mealient.data.add.AddRecipeInfo
import gq.kirmanak.mealient.datasource.models.AddRecipeInstruction import gq.kirmanak.mealient.data.add.AddRecipeIngredientInfo
import gq.kirmanak.mealient.datasource.models.AddRecipeRequest import gq.kirmanak.mealient.data.add.AddRecipeInstructionInfo
import gq.kirmanak.mealient.datasource.models.AddRecipeSettings import gq.kirmanak.mealient.data.add.AddRecipeSettingsInfo
import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft
import org.junit.Test import org.junit.Test
@@ -22,42 +22,42 @@ class RemoteToLocalMappingsTest {
areCommentsDisabled = true, areCommentsDisabled = true,
) )
val expected = AddRecipeRequest( val expected = AddRecipeInfo(
name = "Recipe name", name = "Recipe name",
description = "Recipe description", description = "Recipe description",
recipeYield = "Recipe yield", recipeYield = "Recipe yield",
recipeIngredient = listOf( recipeIngredient = listOf(
AddRecipeIngredient(note = "Recipe ingredient 1"), AddRecipeIngredientInfo(note = "Recipe ingredient 1"),
AddRecipeIngredient(note = "Recipe ingredient 2") AddRecipeIngredientInfo(note = "Recipe ingredient 2")
), ),
recipeInstructions = listOf( recipeInstructions = listOf(
AddRecipeInstruction(text = "Recipe instruction 1"), AddRecipeInstructionInfo(text = "Recipe instruction 1"),
AddRecipeInstruction(text = "Recipe instruction 2") AddRecipeInstructionInfo(text = "Recipe instruction 2")
), ),
settings = AddRecipeSettings( settings = AddRecipeSettingsInfo(
public = false, public = false,
disableComments = true, disableComments = true,
) )
) )
assertThat(input.toAddRecipeRequest()).isEqualTo(expected) assertThat(input.toAddRecipeInfo()).isEqualTo(expected)
} }
@Test @Test
fun `when toDraft then fills fields correctly`() { fun `when toDraft then fills fields correctly`() {
val request = AddRecipeRequest( val request = AddRecipeInfo(
name = "Recipe name", name = "Recipe name",
description = "Recipe description", description = "Recipe description",
recipeYield = "Recipe yield", recipeYield = "Recipe yield",
recipeIngredient = listOf( recipeIngredient = listOf(
AddRecipeIngredient(note = "Recipe ingredient 1"), AddRecipeIngredientInfo(note = "Recipe ingredient 1"),
AddRecipeIngredient(note = "Recipe ingredient 2") AddRecipeIngredientInfo(note = "Recipe ingredient 2")
), ),
recipeInstructions = listOf( recipeInstructions = listOf(
AddRecipeInstruction(text = "Recipe instruction 1"), AddRecipeInstructionInfo(text = "Recipe instruction 1"),
AddRecipeInstruction(text = "Recipe instruction 2") AddRecipeInstructionInfo(text = "Recipe instruction 2")
), ),
settings = AddRecipeSettings( settings = AddRecipeSettingsInfo(
public = false, public = false,
disableComments = true, disableComments = true,
) )

View File

@@ -1,10 +1,13 @@
package gq.kirmanak.mealient.test package gq.kirmanak.mealient.test
import gq.kirmanak.mealient.data.baseurl.ServerVersion
object AuthImplTestData { object AuthImplTestData {
const val TEST_USERNAME = "TEST_USERNAME" const val TEST_USERNAME = "TEST_USERNAME"
const val TEST_PASSWORD = "TEST_PASSWORD" const val TEST_PASSWORD = "TEST_PASSWORD"
const val TEST_BASE_URL = "https://example.com/" const val TEST_BASE_URL = "https://example.com/"
const val TEST_TOKEN = "TEST_TOKEN" const val TEST_TOKEN = "TEST_TOKEN"
const val TEST_AUTH_HEADER = "Bearer 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
} }

View File

@@ -1,133 +1,95 @@
package gq.kirmanak.mealient.test 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.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.LocalDate
import kotlinx.datetime.LocalDateTime import kotlinx.datetime.LocalDateTime
object RecipeImplTestData { object RecipeImplTestData {
val RECIPE_SUMMARY_CAKE = GetRecipeSummaryResponse( val RECIPE_SUMMARY_CAKE = RecipeSummaryInfo(
remoteId = 1, remoteId = "1",
name = "Cake", name = "Cake",
slug = "cake", slug = "cake",
image = "86",
description = "A tasty cake", description = "A tasty cake",
recipeCategories = listOf("dessert", "tasty"),
tags = listOf("gluten", "allergic"),
rating = 4,
dateAdded = LocalDate.parse("2021-11-13"), 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 RECIPE_SUMMARY_PORRIDGE = GetRecipeSummaryResponse( val RECIPE_SUMMARY_PORRIDGE = RecipeSummaryInfo(
remoteId = 2, remoteId = "2",
name = "Porridge", name = "Porridge",
slug = "porridge", slug = "porridge",
image = "89",
description = "A tasty porridge", description = "A tasty porridge",
recipeCategories = listOf("porridge", "tasty"),
tags = listOf("gluten", "milk"),
rating = 5,
dateAdded = LocalDate.parse("2021-11-12"), dateAdded = LocalDate.parse("2021-11-12"),
dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"), dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"),
imageId = "porridge",
) )
val TEST_RECIPE_SUMMARIES = listOf(RECIPE_SUMMARY_CAKE, RECIPE_SUMMARY_PORRIDGE) val TEST_RECIPE_SUMMARIES = listOf(RECIPE_SUMMARY_CAKE, RECIPE_SUMMARY_PORRIDGE)
val CAKE_RECIPE_SUMMARY_ENTITY = RecipeSummaryEntity( val CAKE_RECIPE_SUMMARY_ENTITY = RecipeSummaryEntity(
remoteId = 1, remoteId = "1",
name = "Cake", name = "Cake",
slug = "cake", slug = "cake",
image = "86",
description = "A tasty cake", description = "A tasty cake",
rating = 4,
dateAdded = LocalDate.parse("2021-11-13"), 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( val PORRIDGE_RECIPE_SUMMARY_ENTITY = RecipeSummaryEntity(
remoteId = 2, remoteId = "2",
name = "Porridge", name = "Porridge",
slug = "porridge", slug = "porridge",
image = "89",
description = "A tasty porridge", description = "A tasty porridge",
rating = 5,
dateAdded = LocalDate.parse("2021-11-12"), dateAdded = LocalDate.parse("2021-11-12"),
dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"), dateUpdated = LocalDateTime.parse("2021-10-13T17:35:23"),
imageId = "porridge",
) )
private val SUGAR_INGREDIENT = GetRecipeIngredientResponse( private val SUGAR_INGREDIENT = RecipeIngredientInfo(
title = "Sugar",
note = "2 oz of white sugar", note = "2 oz of white sugar",
unit = "",
food = "",
disableAmount = true,
quantity = 1
) )
val BREAD_INGREDIENT = GetRecipeIngredientResponse( val BREAD_INGREDIENT = RecipeIngredientInfo(
title = "Bread",
note = "2 oz of white bread", note = "2 oz of white bread",
unit = "",
food = "",
disableAmount = false,
quantity = 2
) )
private val MILK_INGREDIENT = GetRecipeIngredientResponse( private val MILK_INGREDIENT = RecipeIngredientInfo(
title = "Milk",
note = "2 oz of white milk", note = "2 oz of white milk",
unit = "",
food = "",
disableAmount = true,
quantity = 3
) )
val MIX_INSTRUCTION = GetRecipeInstructionResponse( val MIX_INSTRUCTION = RecipeInstructionInfo(
title = "Mix",
text = "Mix the ingredients" text = "Mix the ingredients"
) )
private val BAKE_INSTRUCTION = GetRecipeInstructionResponse( private val BAKE_INSTRUCTION = RecipeInstructionInfo(
title = "Bake",
text = "Bake the ingredients" text = "Bake the ingredients"
) )
private val BOIL_INSTRUCTION = GetRecipeInstructionResponse( private val BOIL_INSTRUCTION = RecipeInstructionInfo(
title = "Boil",
text = "Boil the ingredients" text = "Boil the ingredients"
) )
val GET_CAKE_RESPONSE = GetRecipeResponse( val GET_CAKE_RESPONSE = FullRecipeInfo(
remoteId = 1, remoteId = "1",
name = "Cake", 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", recipeYield = "4 servings",
recipeIngredients = listOf(SUGAR_INGREDIENT, BREAD_INGREDIENT), recipeIngredients = listOf(SUGAR_INGREDIENT, BREAD_INGREDIENT),
recipeInstructions = listOf(MIX_INSTRUCTION, BAKE_INSTRUCTION) recipeInstructions = listOf(MIX_INSTRUCTION, BAKE_INSTRUCTION)
) )
val GET_PORRIDGE_RESPONSE = GetRecipeResponse( val GET_PORRIDGE_RESPONSE = FullRecipeInfo(
remoteId = 2, remoteId = "2",
name = "Porridge", 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", recipeYield = "3 servings",
recipeIngredients = listOf(SUGAR_INGREDIENT, MILK_INGREDIENT), recipeIngredients = listOf(SUGAR_INGREDIENT, MILK_INGREDIENT),
recipeInstructions = listOf(MIX_INSTRUCTION, BOIL_INSTRUCTION) recipeInstructions = listOf(MIX_INSTRUCTION, BOIL_INSTRUCTION)
@@ -135,46 +97,34 @@ object RecipeImplTestData {
val MIX_CAKE_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( val MIX_CAKE_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity(
localId = 1, localId = 1,
recipeId = 1, recipeId = "1",
title = "Mix",
text = "Mix the ingredients", text = "Mix the ingredients",
) )
private val BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( private val BAKE_CAKE_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity(
localId = 2, localId = 2,
recipeId = 1, recipeId = "1",
title = "Bake",
text = "Bake the ingredients", text = "Bake the ingredients",
) )
private val CAKE_RECIPE_ENTITY = RecipeEntity( private val CAKE_RECIPE_ENTITY = RecipeEntity(
remoteId = 1, remoteId = "1",
recipeYield = "4 servings" recipeYield = "4 servings"
) )
private val CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( private val CAKE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
localId = 1, localId = 1,
recipeId = 1, recipeId = "1",
title = "Sugar",
note = "2 oz of white sugar", note = "2 oz of white sugar",
unit = "",
food = "",
disableAmount = true,
quantity = 1
) )
val CAKE_BREAD_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( val CAKE_BREAD_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
localId = 2, localId = 2,
recipeId = 1, recipeId = "1",
title = "Bread",
note = "2 oz of white bread", 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, recipeEntity = CAKE_RECIPE_ENTITY,
recipeSummaryEntity = CAKE_RECIPE_SUMMARY_ENTITY, recipeSummaryEntity = CAKE_RECIPE_SUMMARY_ENTITY,
recipeIngredients = listOf( recipeIngredients = listOf(
@@ -188,47 +138,35 @@ object RecipeImplTestData {
) )
private val PORRIDGE_RECIPE_ENTITY_FULL = RecipeEntity( private val PORRIDGE_RECIPE_ENTITY_FULL = RecipeEntity(
remoteId = 2, remoteId = "2",
recipeYield = "3 servings" recipeYield = "3 servings"
) )
private val PORRIDGE_MILK_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( private val PORRIDGE_MILK_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
localId = 4, localId = 4,
recipeId = 2, recipeId = "2",
title = "Milk",
note = "2 oz of white milk", note = "2 oz of white milk",
unit = "",
food = "",
disableAmount = true,
quantity = 3
) )
private val PORRIDGE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity( private val PORRIDGE_SUGAR_RECIPE_INGREDIENT_ENTITY = RecipeIngredientEntity(
localId = 3, localId = 3,
recipeId = 2, recipeId = "2",
title = "Sugar",
note = "2 oz of white sugar", note = "2 oz of white sugar",
unit = "",
food = "",
disableAmount = true,
quantity = 1
) )
private val PORRIDGE_MIX_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( private val PORRIDGE_MIX_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity(
localId = 3, localId = 3,
recipeId = 2, recipeId = "2",
title = "Mix",
text = "Mix the ingredients" text = "Mix the ingredients"
) )
private val PORRIDGE_BOIL_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity( private val PORRIDGE_BOIL_RECIPE_INSTRUCTION_ENTITY = RecipeInstructionEntity(
localId = 4, localId = 4,
recipeId = 2, recipeId = "2",
title = "Boil",
text = "Boil the ingredients" text = "Boil the ingredients"
) )
val FULL_PORRIDGE_INFO_ENTITY = FullRecipeInfo( val FULL_PORRIDGE_INFO_ENTITY = FullRecipeEntity(
recipeEntity = PORRIDGE_RECIPE_ENTITY_FULL, recipeEntity = PORRIDGE_RECIPE_ENTITY_FULL,
recipeSummaryEntity = PORRIDGE_RECIPE_SUMMARY_ENTITY, recipeSummaryEntity = PORRIDGE_RECIPE_SUMMARY_ENTITY,
recipeIngredients = listOf( recipeIngredients = listOf(
@@ -240,4 +178,21 @@ object RecipeImplTestData {
PORRIDGE_BOIL_RECIPE_INSTRUCTION_ENTITY, 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),
)
} }

View File

@@ -2,8 +2,8 @@ package gq.kirmanak.mealient.ui.add
import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertThat
import gq.kirmanak.mealient.data.add.AddRecipeRepo import gq.kirmanak.mealient.data.add.AddRecipeRepo
import gq.kirmanak.mealient.datasource.models.AddRecipeRequest
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.test.RecipeImplTestData.PORRIDGE_ADD_RECIPE_INFO
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.coEvery import io.mockk.coEvery
import io.mockk.coVerify import io.mockk.coVerify
@@ -61,21 +61,21 @@ class AddRecipeViewModelTest {
@Test @Test
fun `when preserve then doesn't update UI`() { fun `when preserve then doesn't update UI`() {
coEvery { addRecipeRepo.addRecipeRequestFlow } returns flowOf(AddRecipeRequest()) coEvery { addRecipeRepo.addRecipeRequestFlow } returns flowOf(PORRIDGE_ADD_RECIPE_INFO)
subject.preserve(AddRecipeRequest()) subject.preserve(PORRIDGE_ADD_RECIPE_INFO)
coVerify(inverse = true) { addRecipeRepo.addRecipeRequestFlow } coVerify(inverse = true) { addRecipeRepo.addRecipeRequestFlow }
} }
@Test @Test
fun `when preservedAddRecipeRequest without loadPreservedRequest then empty`() = runTest { 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() } val actual = withTimeoutOrNull(10) { subject.preservedAddRecipeRequest.firstOrNull() }
assertThat(actual).isNull() assertThat(actual).isNull()
} }
@Test @Test
fun `when loadPreservedRequest then updates preservedAddRecipeRequest`() = runTest { fun `when loadPreservedRequest then updates preservedAddRecipeRequest`() = runTest {
val expected = AddRecipeRequest() val expected = PORRIDGE_ADD_RECIPE_INFO
coEvery { addRecipeRepo.addRecipeRequestFlow } returns flowOf(expected) coEvery { addRecipeRepo.addRecipeRequestFlow } returns flowOf(expected)
subject.loadPreservedRequest() subject.loadPreservedRequest()
assertThat(subject.preservedAddRecipeRequest.first()).isSameInstanceAs(expected) assertThat(subject.preservedAddRecipeRequest.first()).isSameInstanceAs(expected)
@@ -83,7 +83,7 @@ class AddRecipeViewModelTest {
@Test @Test
fun `when clear then updates preservedAddRecipeRequest`() = runTest { fun `when clear then updates preservedAddRecipeRequest`() = runTest {
val expected = AddRecipeRequest() val expected = PORRIDGE_ADD_RECIPE_INFO
coEvery { addRecipeRepo.addRecipeRequestFlow } returns flowOf(expected) coEvery { addRecipeRepo.addRecipeRequestFlow } returns flowOf(expected)
subject.clear() subject.clear()
assertThat(subject.preservedAddRecipeRequest.first()).isSameInstanceAs(expected) assertThat(subject.preservedAddRecipeRequest.first()).isSameInstanceAs(expected)

View File

@@ -1,10 +1,11 @@
package gq.kirmanak.mealient.ui.baseurl 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.VersionDataSource
import gq.kirmanak.mealient.data.baseurl.VersionInfo import gq.kirmanak.mealient.data.baseurl.VersionInfo
import gq.kirmanak.mealient.logging.Logger import gq.kirmanak.mealient.logging.Logger
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL import gq.kirmanak.mealient.test.AuthImplTestData.TEST_BASE_URL
import gq.kirmanak.mealient.test.AuthImplTestData.TEST_VERSION
import gq.kirmanak.mealient.test.RobolectricTest import gq.kirmanak.mealient.test.RobolectricTest
import io.mockk.MockKAnnotations import io.mockk.MockKAnnotations
import io.mockk.coEvery import io.mockk.coEvery
@@ -20,7 +21,7 @@ import org.junit.Test
class BaseURLViewModelTest : RobolectricTest() { class BaseURLViewModelTest : RobolectricTest() {
@MockK(relaxUnitFun = true) @MockK(relaxUnitFun = true)
lateinit var baseURLStorage: BaseURLStorage lateinit var serverInfoRepo: ServerInfoRepo
@MockK @MockK
lateinit var versionDataSource: VersionDataSource lateinit var versionDataSource: VersionDataSource
@@ -33,16 +34,16 @@ class BaseURLViewModelTest : RobolectricTest() {
@Before @Before
fun setUp() { fun setUp() {
MockKAnnotations.init(this) MockKAnnotations.init(this)
subject = BaseURLViewModel(baseURLStorage, versionDataSource, logger) subject = BaseURLViewModel(serverInfoRepo, versionDataSource, logger)
} }
@Test @Test
fun `when saveBaseUrl and getVersionInfo returns result then saves to storage`() = runTest { fun `when saveBaseUrl and getVersionInfo returns result then saves to storage`() = runTest {
coEvery { coEvery {
versionDataSource.getVersionInfo(eq(TEST_BASE_URL)) versionDataSource.getVersionInfo(eq(TEST_BASE_URL))
} returns VersionInfo(true, "0.5.6", true) } returns VersionInfo(TEST_VERSION)
subject.saveBaseUrl(TEST_BASE_URL) subject.saveBaseUrl(TEST_BASE_URL)
advanceUntilIdle() advanceUntilIdle()
coVerify { baseURLStorage.storeBaseURL(eq(TEST_BASE_URL)) } coVerify { serverInfoRepo.storeBaseURL(eq(TEST_BASE_URL), eq(TEST_VERSION)) }
} }
} }

View File

@@ -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')"
]
}
}

View File

@@ -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')"
]
}
}

View File

@@ -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')"
]
}
}

View File

@@ -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')"
]
}
}

View File

@@ -1,19 +1,13 @@
package gq.kirmanak.mealient.database package gq.kirmanak.mealient.database
import androidx.room.AutoMigration import androidx.room.*
import androidx.room.Database import androidx.room.migration.AutoMigrationSpec
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import gq.kirmanak.mealient.database.recipe.RecipeDao import gq.kirmanak.mealient.database.recipe.RecipeDao
import gq.kirmanak.mealient.database.recipe.entity.* import gq.kirmanak.mealient.database.recipe.entity.*
@Database( @Database(
version = 2, version = 6,
entities = [ entities = [
CategoryEntity::class,
CategoryRecipeEntity::class,
TagEntity::class,
TagRecipeEntity::class,
RecipeSummaryEntity::class, RecipeSummaryEntity::class,
RecipeEntity::class, RecipeEntity::class,
RecipeIngredientEntity::class, RecipeIngredientEntity::class,
@@ -21,10 +15,29 @@ import gq.kirmanak.mealient.database.recipe.entity.*
], ],
exportSchema = true, exportSchema = true,
autoMigrations = [ 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) @TypeConverters(RoomTypeConverters::class)
abstract class AppDb : RoomDatabase() { abstract class AppDb : RoomDatabase() {
abstract fun recipeDao(): RecipeDao 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
} }

View File

@@ -17,6 +17,8 @@ interface DatabaseModule {
@Provides @Provides
@Singleton @Singleton
fun createDb(@ApplicationContext context: Context): AppDb = 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()
} }
} }

View File

@@ -6,54 +6,18 @@ import gq.kirmanak.mealient.database.recipe.entity.*
@Dao @Dao
interface RecipeDao { interface RecipeDao {
@Query("SELECT * FROM tags")
suspend fun queryAllTags(): List<TagEntity>
@Query("SELECT * FROM categories")
suspend fun queryAllCategories(): List<CategoryEntity>
@Query("SELECT * FROM recipe_summaries ORDER BY date_added DESC") @Query("SELECT * FROM recipe_summaries ORDER BY date_added DESC")
fun queryRecipesByPages(): PagingSource<Int, RecipeSummaryEntity> fun queryRecipesByPages(): PagingSource<Int, RecipeSummaryEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecipe(recipeSummaryEntity: RecipeSummaryEntity) 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<TagRecipeEntity>)
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insertCategoryRecipeEntities(categoryRecipeEntities: Set<CategoryRecipeEntity>)
@Query("DELETE FROM recipe_summaries") @Query("DELETE FROM recipe_summaries")
suspend fun removeAllRecipes() 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") @Query("SELECT * FROM recipe_summaries ORDER BY date_updated DESC")
suspend fun queryAllRecipes(): List<RecipeSummaryEntity> suspend fun queryAllRecipes(): List<RecipeSummaryEntity>
@Query("SELECT * FROM category_recipe")
suspend fun queryAllCategoryRecipes(): List<CategoryRecipeEntity>
@Query("SELECT * FROM tag_recipe")
suspend fun queryAllTagRecipes(): List<TagRecipeEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertRecipe(recipe: RecipeEntity) suspend fun insertRecipe(recipe: RecipeEntity)
@@ -66,11 +30,11 @@ interface RecipeDao {
@Transaction @Transaction
@SuppressWarnings(RoomWarnings.CURSOR_MISMATCH) // The lint is wrong, the columns are actually used @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") @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") @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") @Query("DELETE FROM recipe_instruction WHERE recipe_id = :recipeId")
suspend fun deleteRecipeInstructions(recipeId: Long) suspend fun deleteRecipeInstructions(recipeId: String)
} }

View File

@@ -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,
)

View File

@@ -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
)

View File

@@ -3,7 +3,7 @@ package gq.kirmanak.mealient.database.recipe.entity
import androidx.room.Embedded import androidx.room.Embedded
import androidx.room.Relation import androidx.room.Relation
data class FullRecipeInfo( data class FullRecipeEntity(
@Embedded val recipeEntity: RecipeEntity, @Embedded val recipeEntity: RecipeEntity,
@Relation( @Relation(
parentColumn = "remote_id", parentColumn = "remote_id",

View File

@@ -6,6 +6,6 @@ import androidx.room.PrimaryKey
@Entity(tableName = "recipe") @Entity(tableName = "recipe")
data class RecipeEntity( 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, @ColumnInfo(name = "recipe_yield") val recipeYield: String,
) )

View File

@@ -7,11 +7,6 @@ import androidx.room.PrimaryKey
@Entity(tableName = "recipe_ingredient") @Entity(tableName = "recipe_ingredient")
data class RecipeIngredientEntity( data class RecipeIngredientEntity(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
@ColumnInfo(name = "recipe_id") val recipeId: Long, @ColumnInfo(name = "recipe_id") val recipeId: String,
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "note") val note: 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,
) )

View File

@@ -7,7 +7,6 @@ import androidx.room.PrimaryKey
@Entity(tableName = "recipe_instruction") @Entity(tableName = "recipe_instruction")
data class RecipeInstructionEntity( data class RecipeInstructionEntity(
@PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0, @PrimaryKey(autoGenerate = true) @ColumnInfo(name = "local_id") val localId: Long = 0,
@ColumnInfo(name = "recipe_id") val recipeId: Long, @ColumnInfo(name = "recipe_id") val recipeId: String,
@ColumnInfo(name = "title") val title: String,
@ColumnInfo(name = "text") val text: String, @ColumnInfo(name = "text") val text: String,
) )

View File

@@ -8,16 +8,11 @@ import kotlinx.datetime.LocalDateTime
@Entity(tableName = "recipe_summaries") @Entity(tableName = "recipe_summaries")
data class RecipeSummaryEntity( 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 = "name") val name: String,
@ColumnInfo(name = "slug") val slug: String, @ColumnInfo(name = "slug") val slug: String,
@ColumnInfo(name = "image") val image: String?,
@ColumnInfo(name = "description") val description: String, @ColumnInfo(name = "description") val description: String,
@ColumnInfo(name = "rating") val rating: Int?,
@ColumnInfo(name = "date_added") val dateAdded: LocalDate, @ColumnInfo(name = "date_added") val dateAdded: LocalDate,
@ColumnInfo(name = "date_updated") val dateUpdated: LocalDateTime @ColumnInfo(name = "date_updated") val dateUpdated: LocalDateTime,
) { @ColumnInfo(name = "image_id") val imageId: String?,
override fun toString(): String { )
return "RecipeSummaryEntity(remoteId=$remoteId, name='$name')"
}
}

View File

@@ -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
)

View File

@@ -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
)

View File

@@ -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 <T> runCatchingExceptCancel(block: () -> T): Result<T> = try {
Result.success(block())
} catch (e: CancellationException) {
throw e
} catch (e: Throwable) {
Result.failure(e)
}
@OptIn(ExperimentalSerializationApi::class)
inline fun <reified R> ResponseBody.decode(json: Json): R = json.decodeFromStream(byteStream())

View File

@@ -6,6 +6,12 @@ import dagger.Module
import dagger.Provides import dagger.Provides
import dagger.hilt.InstallIn import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent 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.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
@@ -49,9 +55,13 @@ interface DataSourceModule {
@Provides @Provides
@Singleton @Singleton
fun provideMealieService(retrofit: Retrofit): MealieService = fun provideMealieService(retrofit: Retrofit): MealieServiceV0 =
retrofit.create() retrofit.create()
@Provides
@Singleton
fun provideMealieServiceV1(retrofit: Retrofit): MealieServiceV1 =
retrofit.create()
} }
@Binds @Binds
@@ -64,5 +74,13 @@ interface DataSourceModule {
@Binds @Binds
@Singleton @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
} }

View File

@@ -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<ErrorDetail>()
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<GetRecipeSummaryResponse> = 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 <T> makeCall(
crossinline block: suspend MealieService.() -> T,
crossinline logMethod: () -> String,
crossinline logParameters: () -> String,
): Result<T> {
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 <reified R> ResponseBody.decode(): R = json.decodeFromStream(byteStream())
}
private fun <T> Result<T>.getOrThrowUnauthorized(): T = getOrElse {
throw if (it is HttpException && it.code() in listOf(401, 403)) {
NetworkError.Unauthorized(it)
} else {
it
}
}

View File

@@ -1,4 +1,4 @@
package gq.kirmanak.mealient.datasource.models package gq.kirmanak.mealient.datasource
sealed class NetworkError(cause: Throwable) : RuntimeException(cause) { sealed class NetworkError(cause: Throwable) : RuntimeException(cause) {
class Unauthorized(cause: Throwable) : NetworkError(cause) class Unauthorized(cause: Throwable) : NetworkError(cause)

View File

@@ -0,0 +1,17 @@
package gq.kirmanak.mealient.datasource
interface NetworkRequestWrapper {
suspend fun <T> makeCall(
block: suspend () -> T,
logMethod: () -> String,
logParameters: () -> String,
): Result<T>
suspend fun <T> makeCallAndHandleUnauthorized(
block: suspend () -> T,
logMethod: () -> String,
logParameters: () -> String,
): T
}

View File

@@ -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 <T> makeCall(
block: suspend () -> T,
logMethod: () -> String,
logParameters: () -> String,
): Result<T> {
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 <T> 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
}
}
}

View File

@@ -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<AddRecipeIngredient> = emptyList(),
@SerialName("recipeInstructions") val recipeInstructions: List<AddRecipeInstruction> = emptyList(),
@SerialName("slug") val slug: String = "",
@SerialName("filePath") val filePath: String = "",
@SerialName("tags") val tags: List<String> = emptyList(),
@SerialName("categories") val categories: List<String> = emptyList(),
@SerialName("notes") val notes: List<AddRecipeNote> = emptyList(),
@SerialName("extras") val extras: Map<String, String> = emptyMap(),
@SerialName("assets") val assets: List<String> = emptyList(),
@SerialName("settings") val settings: AddRecipeSettings = AddRecipeSettings(),
)
@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,
)

View File

@@ -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)

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -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<String>,
@SerialName("tags") val tags: List<String>,
@SerialName("rating") val rating: Int?,
@SerialName("dateAdded") val dateAdded: LocalDate,
@SerialName("dateUpdated") val dateUpdated: LocalDateTime,
@SerialName("recipeYield") val recipeYield: String = "",
@SerialName("recipeIngredient") val recipeIngredients: List<GetRecipeIngredientResponse>,
@SerialName("recipeInstructions") val recipeInstructions: List<GetRecipeInstructionResponse>,
)

View File

@@ -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<String>,
@SerialName("tags") val tags: List<String>,
@SerialName("rating") val rating: Int?,
@SerialName("dateAdded") val dateAdded: LocalDate,
@SerialName("dateUpdated") val dateUpdated: LocalDateTime
) {
override fun toString(): String {
return "GetRecipeSummaryResponse(remoteId=$remoteId, name='$name')"
}
}

View File

@@ -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)

View File

@@ -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,
)

View File

@@ -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.v0.models.AddRecipeRequestV0
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0
import gq.kirmanak.mealient.datasource.models.VersionResponse import gq.kirmanak.mealient.datasource.v0.models.VersionResponseV0
interface MealieDataSource { interface MealieDataSourceV0 {
suspend fun addRecipe( suspend fun addRecipe(
baseUrl: String, baseUrl: String,
token: String?, token: String?,
recipe: AddRecipeRequest, recipe: AddRecipeRequestV0,
): String ): String
/** /**
@@ -24,18 +24,18 @@ interface MealieDataSource {
suspend fun getVersionInfo( suspend fun getVersionInfo(
baseUrl: String, baseUrl: String,
): VersionResponse ): VersionResponseV0
suspend fun requestRecipes( suspend fun requestRecipes(
baseUrl: String, baseUrl: String,
token: String?, token: String?,
start: Int, start: Int,
limit: Int, limit: Int,
): List<GetRecipeSummaryResponse> ): List<GetRecipeSummaryResponseV0>
suspend fun requestRecipeInfo( suspend fun requestRecipeInfo(
baseUrl: String, baseUrl: String,
token: String?, token: String?,
slug: String, slug: String,
): GetRecipeResponse ): GetRecipeResponseV0
} }

View File

@@ -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<ErrorDetailV0>(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<GetRecipeSummaryResponseV0> = 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" }
)
}

View File

@@ -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.DataSourceModule.Companion.AUTHORIZATION_HEADER_NAME
import gq.kirmanak.mealient.datasource.models.* import gq.kirmanak.mealient.datasource.v0.models.*
import retrofit2.http.* import retrofit2.http.*
interface MealieService { interface MealieServiceV0 {
@FormUrlEncoded @FormUrlEncoded
@POST @POST
@@ -12,19 +12,19 @@ interface MealieService {
@Url url: String, @Url url: String,
@Field("username") username: String, @Field("username") username: String,
@Field("password") password: String, @Field("password") password: String,
): GetTokenResponse ): GetTokenResponseV0
@POST @POST
suspend fun addRecipe( suspend fun addRecipe(
@Url url: String, @Url url: String,
@Header(AUTHORIZATION_HEADER_NAME) token: String?, @Header(AUTHORIZATION_HEADER_NAME) token: String?,
@Body addRecipeRequest: AddRecipeRequest, @Body addRecipeRequestV0: AddRecipeRequestV0,
): String ): String
@GET @GET
suspend fun getVersion( suspend fun getVersion(
@Url url: String, @Url url: String,
): VersionResponse ): VersionResponseV0
@GET @GET
suspend fun getRecipeSummary( suspend fun getRecipeSummary(
@@ -32,11 +32,11 @@ interface MealieService {
@Header(AUTHORIZATION_HEADER_NAME) token: String?, @Header(AUTHORIZATION_HEADER_NAME) token: String?,
@Query("start") start: Int, @Query("start") start: Int,
@Query("limit") limit: Int, @Query("limit") limit: Int,
): List<GetRecipeSummaryResponse> ): List<GetRecipeSummaryResponseV0>
@GET @GET
suspend fun getRecipe( suspend fun getRecipe(
@Url url: String, @Url url: String,
@Header(AUTHORIZATION_HEADER_NAME) token: String?, @Header(AUTHORIZATION_HEADER_NAME) token: String?,
): GetRecipeResponse ): GetRecipeResponseV0
} }

View File

@@ -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<AddRecipeIngredientV0>,
@SerialName("recipeInstructions") val recipeInstructions: List<AddRecipeInstructionV0>,
@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,
)

View File

@@ -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)

View File

@@ -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<GetRecipeIngredientResponseV0>,
@SerialName("recipeInstructions") val recipeInstructions: List<GetRecipeInstructionResponseV0>,
)
@Serializable
data class GetRecipeIngredientResponseV0(
@SerialName("note") val note: String = "",
)
@Serializable
data class GetRecipeInstructionResponseV0(
@SerialName("text") val text: String,
)

View File

@@ -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
)

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -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<GetRecipeSummaryResponseV1>
suspend fun requestRecipeInfo(
baseUrl: String,
token: String?,
slug: String,
): GetRecipeResponseV1
}

View File

@@ -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<ErrorDetailV1>(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<GetRecipeSummaryResponseV1> = 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" }
)
}

View File

@@ -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
}

View File

@@ -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,
)

View File

@@ -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,
)

View File

@@ -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<GetRecipeIngredientResponseV1>,
@SerialName("recipeInstructions") val recipeInstructions: List<GetRecipeInstructionResponseV1>,
)
@Serializable
data class GetRecipeIngredientResponseV1(
@SerialName("note") val note: String = "",
)
@Serializable
data class GetRecipeInstructionResponseV1(
@SerialName("text") val text: String,
)

Some files were not shown because too many files have changed in this diff Show More