Implement adding recipes through app

This commit is contained in:
Kirill Kamakin
2022-05-26 13:29:10 +02:00
parent 986d8f377f
commit e18f726da5
37 changed files with 1105 additions and 78 deletions

View File

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

View File

@@ -0,0 +1,15 @@
package gq.kirmanak.mealient.data.add
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
import kotlinx.coroutines.flow.Flow
interface AddRecipeRepo {
val addRecipeRequestFlow: Flow<AddRecipeRequest>
suspend fun preserve(recipe: AddRecipeRequest)
suspend fun clear()
suspend fun saveRecipe(): String
}

View File

@@ -0,0 +1,13 @@
package gq.kirmanak.mealient.data.add
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
import kotlinx.coroutines.flow.Flow
interface AddRecipeStorage {
val updates: Flow<AddRecipeRequest>
suspend fun save(addRecipeRequest: AddRecipeRequest)
suspend fun clear()
}

View File

@@ -0,0 +1,25 @@
package gq.kirmanak.mealient.data.add.impl
import gq.kirmanak.mealient.data.add.AddRecipeDataSource
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
import gq.kirmanak.mealient.data.network.ServiceFactory
import gq.kirmanak.mealient.extensions.logAndMapErrors
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AddRecipeDataSourceImpl @Inject constructor(
private val addRecipeServiceFactory: ServiceFactory<AddRecipeService>,
) : AddRecipeDataSource {
override suspend fun addRecipe(recipe: AddRecipeRequest): String {
Timber.v("addRecipe() called with: recipe = $recipe")
val service = addRecipeServiceFactory.provideService()
val response = logAndMapErrors(
block = { service.addRecipe(recipe) }, logProvider = { "addRecipe: can't add recipe" }
)
Timber.v("addRecipe() response = $response")
return response
}
}

View File

@@ -0,0 +1,36 @@
package gq.kirmanak.mealient.data.add.impl
import gq.kirmanak.mealient.data.add.AddRecipeDataSource
import gq.kirmanak.mealient.data.add.AddRecipeRepo
import gq.kirmanak.mealient.data.add.AddRecipeStorage
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AddRecipeRepoImpl @Inject constructor(
private val addRecipeDataSource: AddRecipeDataSource,
private val addRecipeStorage: AddRecipeStorage,
) : AddRecipeRepo {
override val addRecipeRequestFlow: Flow<AddRecipeRequest>
get() = addRecipeStorage.updates
override suspend fun preserve(recipe: AddRecipeRequest) {
Timber.v("preserveRecipe() called with: recipe = $recipe")
addRecipeStorage.save(recipe)
}
override suspend fun clear() {
Timber.v("clear() called")
addRecipeStorage.clear()
}
override suspend fun saveRecipe(): String {
Timber.v("saveRecipe() called")
return addRecipeDataSource.addRecipe(addRecipeRequestFlow.first())
}
}

View File

@@ -0,0 +1,12 @@
package gq.kirmanak.mealient.data.add.impl
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
import retrofit2.http.Body
import retrofit2.http.POST
interface AddRecipeService {
@POST("/api/recipes/create")
suspend fun addRecipe(@Body addRecipeRequest: AddRecipeRequest): String
}

View File

@@ -0,0 +1,30 @@
package gq.kirmanak.mealient.data.add.impl
import androidx.datastore.core.DataStore
import gq.kirmanak.mealient.data.add.AddRecipeStorage
import gq.kirmanak.mealient.data.add.models.AddRecipeInput
import gq.kirmanak.mealient.data.add.models.AddRecipeRequest
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import timber.log.Timber
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class AddRecipeStorageImpl @Inject constructor(
private val dataStore: DataStore<AddRecipeInput>,
) : AddRecipeStorage {
override val updates: Flow<AddRecipeRequest>
get() = dataStore.data.map { AddRecipeRequest(it) }
override suspend fun save(addRecipeRequest: AddRecipeRequest) {
Timber.v("saveRecipeInput() called with: addRecipeRequest = $addRecipeRequest")
dataStore.updateData { addRecipeRequest.toInput() }
}
override suspend fun clear() {
Timber.v("clearRecipeInput() called")
dataStore.updateData { AddRecipeInput.getDefaultInstance() }
}
}

View File

@@ -0,0 +1,19 @@
package gq.kirmanak.mealient.data.add.models
import androidx.datastore.core.CorruptionException
import androidx.datastore.core.Serializer
import com.google.protobuf.InvalidProtocolBufferException
import java.io.InputStream
import java.io.OutputStream
object AddRecipeInputSerializer : Serializer<AddRecipeInput> {
override val defaultValue: AddRecipeInput = AddRecipeInput.getDefaultInstance()
override suspend fun readFrom(input: InputStream): AddRecipeInput = try {
AddRecipeInput.parseFrom(input)
} catch (e: InvalidProtocolBufferException) {
throw CorruptionException("Can't read proto file", e)
}
override suspend fun writeTo(t: AddRecipeInput, output: OutputStream) = t.writeTo(output)
}

View File

@@ -0,0 +1,76 @@
package gq.kirmanak.mealient.data.add.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(),
) {
constructor(input: AddRecipeInput) : this(
name = input.recipeName,
description = input.recipeDescription,
recipeYield = input.recipeYield,
recipeIngredient = input.recipeIngredientsList.map { AddRecipeIngredient(note = it) },
recipeInstructions = input.recipeInstructionsList.map { AddRecipeInstruction(text = it) },
settings = AddRecipeSettings(
public = input.isRecipePublic,
disableComments = input.areCommentsDisabled,
)
)
fun toInput(): AddRecipeInput = AddRecipeInput.newBuilder()
.setRecipeName(name)
.setRecipeDescription(description)
.setRecipeYield(recipeYield)
.setIsRecipePublic(settings.public)
.setAreCommentsDisabled(settings.disableComments)
.addAllRecipeIngredients(recipeIngredient.map { it.note })
.addAllRecipeInstructions(recipeInstructions.map { it.text })
.build()
}
@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

@@ -6,8 +6,7 @@ import gq.kirmanak.mealient.data.network.NetworkError.NotMealie
import gq.kirmanak.mealient.data.network.NetworkError.Unauthorized
import gq.kirmanak.mealient.data.network.ServiceFactory
import gq.kirmanak.mealient.extensions.decodeErrorBodyOrNull
import gq.kirmanak.mealient.extensions.mapToNetworkError
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import gq.kirmanak.mealient.extensions.logAndMapErrors
import kotlinx.serialization.json.Json
import retrofit2.HttpException
import retrofit2.Response
@@ -34,12 +33,10 @@ class AuthDataSourceImpl @Inject constructor(
authService: AuthService,
username: String,
password: String
): Response<GetTokenResponse> = runCatchingExceptCancel {
authService.getToken(username, password)
}.getOrElse {
Timber.e(it, "sendRequest: can't request token")
throw it.mapToNetworkError()
}
): Response<GetTokenResponse> = logAndMapErrors(
block = { authService.getToken(username = username, password = password) },
logProvider = { "sendRequest: can't get token" },
)
private fun parseToken(
response: Response<GetTokenResponse>

View File

@@ -3,8 +3,7 @@ package gq.kirmanak.mealient.data.baseurl.impl
import gq.kirmanak.mealient.data.baseurl.VersionDataSource
import gq.kirmanak.mealient.data.baseurl.VersionInfo
import gq.kirmanak.mealient.data.network.ServiceFactory
import gq.kirmanak.mealient.extensions.mapToNetworkError
import gq.kirmanak.mealient.extensions.runCatchingExceptCancel
import gq.kirmanak.mealient.extensions.logAndMapErrors
import gq.kirmanak.mealient.extensions.versionInfo
import timber.log.Timber
import javax.inject.Inject
@@ -19,12 +18,10 @@ class VersionDataSourceImpl @Inject constructor(
Timber.v("getVersionInfo() called with: baseUrl = $baseUrl")
val service = serviceFactory.provideService(baseUrl)
val response = runCatchingExceptCancel {
service.getVersion()
}.getOrElse {
Timber.e(it, "getVersionInfo: can't request version")
throw it.mapToNetworkError()
}
val response = logAndMapErrors(
block = { service.getVersion() },
logProvider = { "getVersionInfo: can't request version" }
)
return response.versionInfo()
}