Simplify network layer (#175)

* Use Ktor for network requests

* Remove V0 version

* Remove Retrofit dependency

* Fix url

* Update versions of dependencies

* Revert kotlinx-datetime

Due to https://github.com/Kotlin/kotlinx-datetime/issues/304

* Rename leftovers

* Remove OkHttp

* Remove unused manifest

* Remove unused Hilt module

* Fix building empty image URLs

* Use OkHttp as engine for Ktor

* Reduce visibility of internal classes

* Fix first set up test

* Store only auth token, not header

* Remove UnitInfo/FoodInfo/VersionInfo/NewShoppingListItemInfo

* Remove RecipeSummaryInfo and ShoppingListsInfo

* Remove FullShoppingListInfo

* Remove ParseRecipeURLInfo

* Remove FullRecipeInfo

* Sign out if access token does not work

* Rename getVersionInfo method

* Update version name
This commit is contained in:
Kirill Kamakin
2023-11-05 15:01:19 +01:00
committed by GitHub
parent 888783bf14
commit 5ed1acb678
144 changed files with 1216 additions and 2796 deletions

View File

@@ -2,8 +2,7 @@ package gq.kirmanak.mealient.datasource
interface AuthenticationProvider {
suspend fun getAuthHeader(): String?
suspend fun getAuthToken(): String?
suspend fun logout()
}

View File

@@ -1,8 +0,0 @@
package gq.kirmanak.mealient.datasource
import okhttp3.Cache
interface CacheBuilder {
fun buildCache(): Cache
}

View File

@@ -1,10 +1,6 @@
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
@@ -18,9 +14,6 @@ inline fun <T> runCatchingExceptCancel(block: () -> T): Result<T> = try {
Result.failure(e)
}
@OptIn(ExperimentalSerializationApi::class)
inline fun <reified R> ResponseBody.decode(json: Json): R = json.decodeFromStream(byteStream())
inline fun <reified T> Throwable.findCauseAsInstanceOf(): T? {
var cause: Throwable? = this
var previousCause: Throwable? = null

View File

@@ -1,32 +1,17 @@
package gq.kirmanak.mealient.datasource
import com.jakewharton.retrofit2.converter.kotlinx.serialization.asConverterFactory
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import gq.kirmanak.mealient.datasource.impl.AuthInterceptor
import gq.kirmanak.mealient.datasource.impl.BaseUrlInterceptor
import gq.kirmanak.mealient.datasource.impl.CacheBuilderImpl
import gq.kirmanak.mealient.datasource.impl.MealieDataSourceImpl
import gq.kirmanak.mealient.datasource.impl.MealieServiceKtor
import gq.kirmanak.mealient.datasource.impl.NetworkRequestWrapperImpl
import gq.kirmanak.mealient.datasource.impl.OkHttpBuilderImpl
import gq.kirmanak.mealient.datasource.impl.RetrofitBuilder
import gq.kirmanak.mealient.datasource.impl.TrustedCertificatesStoreImpl
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0
import gq.kirmanak.mealient.datasource.v0.MealieDataSourceV0Impl
import gq.kirmanak.mealient.datasource.v0.MealieServiceV0
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1
import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1Impl
import gq.kirmanak.mealient.datasource.v1.MealieServiceV1
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient
import retrofit2.Converter
import retrofit2.Retrofit
import retrofit2.create
import javax.inject.Singleton
@Module
@@ -43,59 +28,22 @@ internal interface DataSourceModule {
encodeDefaults = true
}
@OptIn(ExperimentalSerializationApi::class)
@Provides
@Singleton
fun provideConverterFactory(json: Json): Converter.Factory =
json.asConverterFactory("application/json".toMediaType())
@Provides
@Singleton
fun provideOkHttp(okHttpBuilder: OkHttpBuilder): OkHttpClient =
fun provideOkHttp(okHttpBuilder: OkHttpBuilderImpl): OkHttpClient =
okHttpBuilder.buildOkHttp()
@Provides
@Singleton
fun provideRetrofit(retrofitBuilder: RetrofitBuilder): Retrofit {
// Fake base URL which will be replaced later by BaseUrlInterceptor
// Solution was suggested here https://github.com/square/retrofit/issues/2161#issuecomment-274204152
return retrofitBuilder.buildRetrofit("http://localhost/")
}
@Provides
@Singleton
fun provideMealieService(retrofit: Retrofit): MealieServiceV0 =
retrofit.create()
@Provides
@Singleton
fun provideMealieServiceV1(retrofit: Retrofit): MealieServiceV1 =
retrofit.create()
}
@Binds
fun bindCacheBuilder(cacheBuilderImpl: CacheBuilderImpl): CacheBuilder
fun bindMealieDataSource(mealientDataSourceImpl: MealieDataSourceImpl): MealieDataSource
@Binds
fun bindOkHttpBuilder(okHttpBuilderImpl: OkHttpBuilderImpl): OkHttpBuilder
@Binds
fun bindMealieDataSource(mealientDataSourceImpl: MealieDataSourceV0Impl): MealieDataSourceV0
@Binds
fun bindMealieDataSourceV1(mealientDataSourceImpl: MealieDataSourceV1Impl): MealieDataSourceV1
fun bindMealieService(impl: MealieServiceKtor): MealieService
@Binds
fun bindNetworkRequestWrapper(networkRequestWrapperImpl: NetworkRequestWrapperImpl): NetworkRequestWrapper
@Binds
@IntoSet
fun bindAuthInterceptor(authInterceptor: AuthInterceptor): LocalInterceptor
@Binds
@IntoSet
fun bindBaseUrlInterceptor(baseUrlInterceptor: BaseUrlInterceptor): LocalInterceptor
@Binds
fun bindTrustedCertificatesStore(impl: TrustedCertificatesStoreImpl): TrustedCertificatesStore
}

View File

@@ -1,12 +0,0 @@
package gq.kirmanak.mealient.datasource
import okhttp3.Interceptor
import okhttp3.OkHttpClient
/**
* Marker interface which is different from [Interceptor] only in how it is handled.
* [Interceptor]s are added as network interceptors to OkHttpClient whereas [LocalInterceptor]s
* are added via [OkHttpClient.Builder.addInterceptor] function. They will observe the
* full call lifecycle, whereas network interceptors will see only the network part.
*/
interface LocalInterceptor : Interceptor

View File

@@ -0,0 +1,78 @@
package gq.kirmanak.mealient.datasource
import gq.kirmanak.mealient.datasource.models.CreateApiTokenRequest
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListsResponse
import gq.kirmanak.mealient.datasource.models.GetUnitsResponse
import gq.kirmanak.mealient.datasource.models.GetUserInfoResponse
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLRequest
import gq.kirmanak.mealient.datasource.models.UpdateRecipeRequest
import gq.kirmanak.mealient.datasource.models.VersionResponse
interface MealieDataSource {
suspend fun createRecipe(
recipe: CreateRecipeRequest,
): String
suspend fun updateRecipe(
slug: String,
recipe: UpdateRecipeRequest,
): GetRecipeResponse
/**
* Tries to acquire authentication token using the provided credentials
*/
suspend fun authenticate(
username: String,
password: String,
): String
suspend fun getVersionInfo(): VersionResponse
suspend fun requestRecipes(
page: Int,
perPage: Int,
): List<GetRecipeSummaryResponse>
suspend fun requestRecipeInfo(
slug: String,
): GetRecipeResponse
suspend fun parseRecipeFromURL(
request: ParseRecipeURLRequest,
): String
suspend fun createApiToken(
request: CreateApiTokenRequest,
): CreateApiTokenResponse
suspend fun requestUserInfo(): GetUserInfoResponse
suspend fun removeFavoriteRecipe(userId: String, recipeSlug: String)
suspend fun addFavoriteRecipe(userId: String, recipeSlug: String)
suspend fun deleteRecipe(slug: String)
suspend fun getShoppingLists(page: Int, perPage: Int): GetShoppingListsResponse
suspend fun getShoppingList(id: String): GetShoppingListResponse
suspend fun deleteShoppingListItem(id: String)
suspend fun updateShoppingListItem(item: GetShoppingListItemResponse)
suspend fun getFoods(): GetFoodsResponse
suspend fun getUnits(): GetUnitsResponse
suspend fun addShoppingListItem(request: CreateShoppingListItemRequest)
}

View File

@@ -0,0 +1,64 @@
package gq.kirmanak.mealient.datasource
import gq.kirmanak.mealient.datasource.models.CreateApiTokenRequest
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
import gq.kirmanak.mealient.datasource.models.GetRecipesResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListsResponse
import gq.kirmanak.mealient.datasource.models.GetTokenResponse
import gq.kirmanak.mealient.datasource.models.GetUnitsResponse
import gq.kirmanak.mealient.datasource.models.GetUserInfoResponse
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLRequest
import gq.kirmanak.mealient.datasource.models.UpdateRecipeRequest
import gq.kirmanak.mealient.datasource.models.VersionResponse
import kotlinx.serialization.json.JsonElement
internal interface MealieService {
suspend fun getToken(username: String, password: String): GetTokenResponse
suspend fun createRecipe(addRecipeRequest: CreateRecipeRequest): String
suspend fun updateRecipe(
addRecipeRequest: UpdateRecipeRequest,
slug: String,
): GetRecipeResponse
suspend fun getVersion(): VersionResponse
suspend fun getRecipeSummary(page: Int, perPage: Int): GetRecipesResponse
suspend fun getRecipe(slug: String): GetRecipeResponse
suspend fun createRecipeFromURL(request: ParseRecipeURLRequest): String
suspend fun createApiToken(request: CreateApiTokenRequest): CreateApiTokenResponse
suspend fun getUserSelfInfo(): GetUserInfoResponse
suspend fun removeFavoriteRecipe(userId: String, recipeSlug: String)
suspend fun addFavoriteRecipe(userId: String, recipeSlug: String)
suspend fun deleteRecipe(slug: String)
suspend fun getShoppingLists(page: Int, perPage: Int): GetShoppingListsResponse
suspend fun getShoppingList(id: String): GetShoppingListResponse
suspend fun getShoppingListItem(id: String): JsonElement
suspend fun updateShoppingListItem(id: String, request: JsonElement)
suspend fun deleteShoppingListItem(id: String)
suspend fun getFoods(perPage: Int): GetFoodsResponse
suspend fun getUnits(perPage: Int): GetUnitsResponse
suspend fun createShoppingListItem(request: CreateShoppingListItemRequest)
}

View File

@@ -1,8 +0,0 @@
package gq.kirmanak.mealient.datasource
import okhttp3.OkHttpClient
interface OkHttpBuilder {
fun buildOkHttp(): OkHttpClient
}

View File

@@ -0,0 +1,6 @@
package gq.kirmanak.mealient.datasource
interface SignOutHandler {
fun signOut()
}

View File

@@ -1,42 +0,0 @@
package gq.kirmanak.mealient.datasource.impl
import androidx.annotation.VisibleForTesting
import gq.kirmanak.mealient.datasource.AuthenticationProvider
import gq.kirmanak.mealient.datasource.LocalInterceptor
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.runBlocking
import okhttp3.Interceptor
import okhttp3.Response
import javax.inject.Inject
import javax.inject.Provider
internal class AuthInterceptor @Inject constructor(
private val logger: Logger,
private val authenticationProviderProvider: Provider<AuthenticationProvider>,
) : LocalInterceptor {
private val authenticationProvider: AuthenticationProvider
get() = authenticationProviderProvider.get()
override fun intercept(chain: Interceptor.Chain): Response {
logger.v { "intercept() was called with: request = ${chain.request()}" }
val header = getAuthHeader()
val request = chain.request().let {
if (header == null) it else it.newBuilder().header(HEADER_NAME, header).build()
}
logger.d { "Sending header $HEADER_NAME=${request.header(HEADER_NAME)}" }
return chain.proceed(request).also {
logger.v { "Response code is ${it.code}" }
if (it.code == 401 && header != null) logout()
}
}
private fun getAuthHeader() = runBlocking { authenticationProvider.getAuthHeader() }
private fun logout() = runBlocking { authenticationProvider.logout() }
companion object {
@VisibleForTesting
const val HEADER_NAME = "Authorization"
}
}

View File

@@ -1,46 +0,0 @@
package gq.kirmanak.mealient.datasource.impl
import gq.kirmanak.mealient.datasource.LocalInterceptor
import gq.kirmanak.mealient.datasource.ServerUrlProvider
import gq.kirmanak.mealient.logging.Logger
import kotlinx.coroutines.runBlocking
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
import javax.inject.Inject
import javax.inject.Provider
internal class BaseUrlInterceptor @Inject constructor(
private val logger: Logger,
private val serverUrlProviderProvider: Provider<ServerUrlProvider>,
) : LocalInterceptor {
private val serverUrlProvider: ServerUrlProvider
get() = serverUrlProviderProvider.get()
override fun intercept(chain: Interceptor.Chain): Response {
logger.v { "intercept() was called with: request = ${chain.request()}" }
val oldRequest = chain.request()
val baseUrl = getBaseUrl()
val correctUrl = oldRequest.url
.newBuilder()
.host(baseUrl.host)
.scheme(baseUrl.scheme)
.port(baseUrl.port)
.build()
val newRequest = oldRequest.newBuilder().url(correctUrl).build()
logger.d { "Replaced ${oldRequest.url} with ${newRequest.url}" }
return chain.proceed(newRequest)
}
private fun getBaseUrl() = runBlocking {
val url = serverUrlProvider.getUrl() ?: throw IOException("Base URL is unknown")
url.runCatching {
toHttpUrl()
}.fold(
onSuccess = { it },
onFailure = { throw IOException(it.message, it) },
)
}
}

View File

@@ -3,7 +3,6 @@ package gq.kirmanak.mealient.datasource.impl
import android.content.Context
import android.os.StatFs
import dagger.hilt.android.qualifiers.ApplicationContext
import gq.kirmanak.mealient.datasource.CacheBuilder
import gq.kirmanak.mealient.logging.Logger
import okhttp3.Cache
import java.io.File
@@ -12,9 +11,9 @@ import javax.inject.Inject
internal class CacheBuilderImpl @Inject constructor(
@ApplicationContext private val context: Context,
private val logger: Logger,
) : CacheBuilder {
) {
override fun buildCache(): Cache {
fun buildCache(): Cache {
val dir = findCacheDir()
return Cache(dir, calculateDiskCacheSize(dir))
}

View File

@@ -1,53 +1,53 @@
package gq.kirmanak.mealient.datasource.v1
package gq.kirmanak.mealient.datasource.impl
import gq.kirmanak.mealient.datasource.MealieDataSource
import gq.kirmanak.mealient.datasource.MealieService
import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.datasource.NetworkRequestWrapper
import gq.kirmanak.mealient.datasource.decode
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenRequestV1
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenResponseV1
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.CreateShoppingListItemRequestV1
import gq.kirmanak.mealient.datasource.v1.models.ErrorDetailV1
import gq.kirmanak.mealient.datasource.v1.models.GetFoodsResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetUnitsResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetUserInfoResponseV1
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import gq.kirmanak.mealient.datasource.models.CreateApiTokenRequest
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
import gq.kirmanak.mealient.datasource.models.ErrorDetail
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListItemResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListsResponse
import gq.kirmanak.mealient.datasource.models.GetUnitsResponse
import gq.kirmanak.mealient.datasource.models.GetUserInfoResponse
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLRequest
import gq.kirmanak.mealient.datasource.models.UpdateRecipeRequest
import gq.kirmanak.mealient.datasource.models.VersionResponse
import io.ktor.client.call.NoTransformationFoundException
import io.ktor.client.call.body
import io.ktor.client.plugins.ResponseException
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonObject
import retrofit2.HttpException
import java.net.ConnectException
import java.net.SocketException
import java.net.SocketTimeoutException
import javax.inject.Inject
class MealieDataSourceV1Impl @Inject constructor(
internal class MealieDataSourceImpl @Inject constructor(
private val networkRequestWrapper: NetworkRequestWrapper,
private val service: MealieServiceV1,
private val json: Json,
) : MealieDataSourceV1 {
private val service: MealieService,
) : MealieDataSource {
override suspend fun createRecipe(
recipe: CreateRecipeRequestV1
recipe: CreateRecipeRequest
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.createRecipe(recipe) },
logMethod = { "createRecipe" },
logParameters = { "recipe = $recipe" }
)
).trim('"')
override suspend fun updateRecipe(
slug: String,
recipe: UpdateRecipeRequestV1
): GetRecipeResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
recipe: UpdateRecipeRequest
): GetRecipeResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.updateRecipe(recipe, slug) },
logMethod = { "updateRecipe" },
logParameters = { "slug = $slug, recipe = $recipe" }
@@ -61,18 +61,17 @@ class MealieDataSourceV1Impl @Inject constructor(
logMethod = { "authenticate" },
logParameters = { "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
val errorDetail = (it as? ResponseException)?.response?.body<ErrorDetail>() ?: throw it
throw if (errorDetail.detail == "Unauthorized") NetworkError.Unauthorized(it) else it
}
override suspend fun getVersionInfo(): VersionResponseV1 = networkRequestWrapper.makeCall(
override suspend fun getVersionInfo(): VersionResponse = networkRequestWrapper.makeCall(
block = { service.getVersion() },
logMethod = { "getVersionInfo" },
).getOrElse {
throw when (it) {
is HttpException, is SerializationException -> NetworkError.NotMealie(it)
is SocketTimeoutException, is ConnectException -> NetworkError.NoServerConnection(it)
is ResponseException, is NoTransformationFoundException -> NetworkError.NotMealie(it)
is SocketTimeoutException, is SocketException -> NetworkError.NoServerConnection(it)
else -> NetworkError.MalformedUrl(it)
}
}
@@ -80,7 +79,7 @@ class MealieDataSourceV1Impl @Inject constructor(
override suspend fun requestRecipes(
page: Int,
perPage: Int
): List<GetRecipeSummaryResponseV1> = networkRequestWrapper.makeCallAndHandleUnauthorized(
): List<GetRecipeSummaryResponse> = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getRecipeSummary(page, perPage) },
logMethod = { "requestRecipes" },
logParameters = { "page = $page, perPage = $perPage" }
@@ -88,14 +87,14 @@ class MealieDataSourceV1Impl @Inject constructor(
override suspend fun requestRecipeInfo(
slug: String
): GetRecipeResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
): GetRecipeResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getRecipe(slug) },
logMethod = { "requestRecipeInfo" },
logParameters = { "slug = $slug" }
)
override suspend fun parseRecipeFromURL(
request: ParseRecipeURLRequestV1
request: ParseRecipeURLRequest
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.createRecipeFromURL(request) },
logMethod = { "parseRecipeFromURL" },
@@ -103,14 +102,14 @@ class MealieDataSourceV1Impl @Inject constructor(
)
override suspend fun createApiToken(
request: CreateApiTokenRequestV1
): CreateApiTokenResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
request: CreateApiTokenRequest
): CreateApiTokenResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.createApiToken(request) },
logMethod = { "createApiToken" },
logParameters = { "request = $request" }
)
override suspend fun requestUserInfo(): GetUserInfoResponseV1 {
override suspend fun requestUserInfo(): GetUserInfoResponse {
return networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getUserSelfInfo() },
logMethod = { "requestUserInfo" },
@@ -146,7 +145,7 @@ class MealieDataSourceV1Impl @Inject constructor(
override suspend fun getShoppingLists(
page: Int,
perPage: Int,
): GetShoppingListsResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
): GetShoppingListsResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getShoppingLists(page, perPage) },
logMethod = { "getShoppingLists" },
logParameters = { "page = $page, perPage = $perPage" }
@@ -154,7 +153,7 @@ class MealieDataSourceV1Impl @Inject constructor(
override suspend fun getShoppingList(
id: String
): GetShoppingListResponseV1 = networkRequestWrapper.makeCallAndHandleUnauthorized(
): GetShoppingListResponse = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getShoppingList(id) },
logMethod = { "getShoppingList" },
logParameters = { "id = $id" }
@@ -186,7 +185,7 @@ class MealieDataSourceV1Impl @Inject constructor(
)
override suspend fun updateShoppingListItem(
item: ShoppingListItemInfo
item: GetShoppingListItemResponse
) {
// Has to be done in two steps because we can't specify only the changed fields
val remoteItem = getShoppingListItem(item.id)
@@ -203,14 +202,14 @@ class MealieDataSourceV1Impl @Inject constructor(
updateShoppingListItem(item.id, JsonObject(updatedItem))
}
override suspend fun getFoods(): GetFoodsResponseV1 {
override suspend fun getFoods(): GetFoodsResponse {
return networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getFoods(perPage = -1) },
logMethod = { "getFoods" },
)
}
override suspend fun getUnits(): GetUnitsResponseV1 {
override suspend fun getUnits(): GetUnitsResponse {
return networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getUnits(perPage = -1) },
logMethod = { "getUnits" },
@@ -218,7 +217,7 @@ class MealieDataSourceV1Impl @Inject constructor(
}
override suspend fun addShoppingListItem(
request: CreateShoppingListItemRequestV1
request: CreateShoppingListItemRequest
) = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.createShoppingListItem(request) },
logMethod = { "addShoppingListItem" },

View File

@@ -0,0 +1,210 @@
package gq.kirmanak.mealient.datasource.impl
import gq.kirmanak.mealient.datasource.MealieService
import gq.kirmanak.mealient.datasource.ServerUrlProvider
import gq.kirmanak.mealient.datasource.models.CreateApiTokenRequest
import gq.kirmanak.mealient.datasource.models.CreateApiTokenResponse
import gq.kirmanak.mealient.datasource.models.CreateRecipeRequest
import gq.kirmanak.mealient.datasource.models.CreateShoppingListItemRequest
import gq.kirmanak.mealient.datasource.models.GetFoodsResponse
import gq.kirmanak.mealient.datasource.models.GetRecipeResponse
import gq.kirmanak.mealient.datasource.models.GetRecipesResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListResponse
import gq.kirmanak.mealient.datasource.models.GetShoppingListsResponse
import gq.kirmanak.mealient.datasource.models.GetTokenResponse
import gq.kirmanak.mealient.datasource.models.GetUnitsResponse
import gq.kirmanak.mealient.datasource.models.GetUserInfoResponse
import gq.kirmanak.mealient.datasource.models.ParseRecipeURLRequest
import gq.kirmanak.mealient.datasource.models.UpdateRecipeRequest
import gq.kirmanak.mealient.datasource.models.VersionResponse
import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.HttpRequestBuilder
import io.ktor.client.request.delete
import io.ktor.client.request.forms.FormDataContent
import io.ktor.client.request.get
import io.ktor.client.request.patch
import io.ktor.client.request.post
import io.ktor.client.request.put
import io.ktor.client.request.setBody
import io.ktor.http.ContentType
import io.ktor.http.URLBuilder
import io.ktor.http.contentType
import io.ktor.http.parameters
import io.ktor.http.path
import io.ktor.http.takeFrom
import kotlinx.serialization.json.JsonElement
import javax.inject.Inject
import javax.inject.Provider
internal class MealieServiceKtor @Inject constructor(
private val httpClient: HttpClient,
private val serverUrlProviderProvider: Provider<ServerUrlProvider>,
) : MealieService {
private val serverUrlProvider: ServerUrlProvider
get() = serverUrlProviderProvider.get()
override suspend fun getToken(username: String, password: String): GetTokenResponse {
val formParameters = parameters {
append("username", username)
append("password", password)
}
return httpClient.post {
endpoint("/api/auth/token")
setBody(FormDataContent(formParameters))
}.body()
}
override suspend fun createRecipe(addRecipeRequest: CreateRecipeRequest): String {
return httpClient.post {
endpoint("/api/recipes")
contentType(ContentType.Application.Json)
setBody(addRecipeRequest)
}.body()
}
override suspend fun updateRecipe(
addRecipeRequest: UpdateRecipeRequest,
slug: String,
): GetRecipeResponse {
return httpClient.patch {
endpoint("/api/recipes/$slug")
contentType(ContentType.Application.Json)
setBody(addRecipeRequest)
}.body()
}
override suspend fun getVersion(): VersionResponse {
return httpClient.get {
endpoint("/api/app/about")
}.body()
}
override suspend fun getRecipeSummary(page: Int, perPage: Int): GetRecipesResponse {
return httpClient.get {
endpoint("/api/recipes") {
parameters.append("page", page.toString())
parameters.append("perPage", perPage.toString())
}
}.body()
}
override suspend fun getRecipe(slug: String): GetRecipeResponse {
return httpClient.get {
endpoint("/api/recipes/$slug")
}.body()
}
override suspend fun createRecipeFromURL(request: ParseRecipeURLRequest): String {
return httpClient.post {
endpoint("/api/recipes/create-url")
contentType(ContentType.Application.Json)
setBody(request)
}.body()
}
override suspend fun createApiToken(request: CreateApiTokenRequest): CreateApiTokenResponse {
return httpClient.post {
endpoint("/api/users/api-tokens")
contentType(ContentType.Application.Json)
setBody(request)
}.body()
}
override suspend fun getUserSelfInfo(): GetUserInfoResponse {
return httpClient.get {
endpoint("/api/users/self")
}.body()
}
override suspend fun removeFavoriteRecipe(userId: String, recipeSlug: String) {
httpClient.delete {
endpoint("/api/users/$userId/favorites/$recipeSlug")
}
}
override suspend fun addFavoriteRecipe(userId: String, recipeSlug: String) {
httpClient.post {
endpoint("/api/users/$userId/favorites/$recipeSlug")
}
}
override suspend fun deleteRecipe(slug: String) {
httpClient.delete {
endpoint("/api/recipes/$slug")
}
}
override suspend fun getShoppingLists(page: Int, perPage: Int): GetShoppingListsResponse {
return httpClient.get {
endpoint("/api/groups/shopping/lists") {
parameters.append("page", page.toString())
parameters.append("perPage", perPage.toString())
}
}.body()
}
override suspend fun getShoppingList(id: String): GetShoppingListResponse {
return httpClient.get {
endpoint("/api/groups/shopping/lists/$id")
}.body()
}
override suspend fun getShoppingListItem(id: String): JsonElement {
return httpClient.get {
endpoint("/api/groups/shopping/items/$id")
}.body()
}
override suspend fun updateShoppingListItem(id: String, request: JsonElement) {
httpClient.put {
endpoint("/api/groups/shopping/items/$id")
contentType(ContentType.Application.Json)
setBody(request)
}
}
override suspend fun deleteShoppingListItem(id: String) {
httpClient.delete {
endpoint("/api/groups/shopping/items/$id")
}
}
override suspend fun getFoods(perPage: Int): GetFoodsResponse {
return httpClient.get {
endpoint("/api/foods") {
parameters.append("perPage", perPage.toString())
}
}.body()
}
override suspend fun getUnits(perPage: Int): GetUnitsResponse {
return httpClient.get {
endpoint("/api/units") {
parameters.append("perPage", perPage.toString())
}
}.body()
}
override suspend fun createShoppingListItem(request: CreateShoppingListItemRequest) {
httpClient.post {
endpoint("/api/groups/shopping/items")
contentType(ContentType.Application.Json)
setBody(request)
}
}
private suspend fun HttpRequestBuilder.endpoint(
path: String,
block: URLBuilder.() -> Unit = {}
) {
val baseUrl = checkNotNull(serverUrlProvider.getUrl()) { "Server URL is not set" }
url {
takeFrom(baseUrl)
path(path)
block()
}
}
}

View File

@@ -4,7 +4,7 @@ import gq.kirmanak.mealient.datasource.NetworkError
import gq.kirmanak.mealient.datasource.NetworkRequestWrapper
import gq.kirmanak.mealient.datasource.runCatchingExceptCancel
import gq.kirmanak.mealient.logging.Logger
import retrofit2.HttpException
import io.ktor.client.plugins.ResponseException
import javax.inject.Inject
internal class NetworkRequestWrapperImpl @Inject constructor(
@@ -49,7 +49,8 @@ internal class NetworkRequestWrapperImpl @Inject constructor(
logMethod: () -> String,
logParameters: (() -> String)?
): T = makeCall(block, logMethod, logParameters).getOrElse {
throw if (it is HttpException && it.code() in listOf(401, 403)) {
val code = (it as? ResponseException)?.response?.status?.value
throw if (code in listOf(401, 403)) {
NetworkError.Unauthorized(it)
} else {
it

View File

@@ -1,55 +1,28 @@
package gq.kirmanak.mealient.datasource.impl
import gq.kirmanak.mealient.datasource.CacheBuilder
import gq.kirmanak.mealient.datasource.LocalInterceptor
import gq.kirmanak.mealient.datasource.OkHttpBuilder
import gq.kirmanak.mealient.logging.Logger
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.TlsVersion
import javax.inject.Inject
import javax.net.ssl.SSLContext
import javax.net.ssl.TrustManager
internal class OkHttpBuilderImpl @Inject constructor(
private val cacheBuilder: CacheBuilder,
private val cacheBuilder: CacheBuilderImpl,
// Use @JvmSuppressWildcards because otherwise dagger can't inject it (https://stackoverflow.com/a/43149382)
private val interceptors: Set<@JvmSuppressWildcards Interceptor>,
private val localInterceptors: Set<@JvmSuppressWildcards LocalInterceptor>,
private val advancedX509TrustManager: AdvancedX509TrustManager,
private val sslSocketFactoryFactory: SslSocketFactoryFactory,
private val logger: Logger,
) : OkHttpBuilder {
) {
override fun buildOkHttp(): OkHttpClient {
logger.v { "buildOkHttp() was called with cacheBuilder = $cacheBuilder, interceptors = $interceptors, localInterceptors = $localInterceptors" }
fun buildOkHttp(): OkHttpClient {
logger.v { "buildOkHttp() was called with cacheBuilder = $cacheBuilder, interceptors = $interceptors" }
val sslContext = buildSSLContext()
sslContext.init(null, arrayOf<TrustManager>(advancedX509TrustManager), null)
val sslSocketFactory = sslContext.socketFactory
val sslSocketFactory = sslSocketFactoryFactory.create()
return OkHttpClient.Builder().apply {
localInterceptors.forEach(::addInterceptor)
interceptors.forEach(::addNetworkInterceptor)
sslSocketFactory(sslSocketFactory, advancedX509TrustManager)
cache(cacheBuilder.buildCache())
}.build()
}
private fun buildSSLContext(): SSLContext {
return runCatching {
SSLContext.getInstance(TlsVersion.TLS_1_3.javaName)
}.recoverCatching {
logger.w { "TLSv1.3 is not supported in this device; falling through TLSv1.2" }
SSLContext.getInstance(TlsVersion.TLS_1_2.javaName)
}.recoverCatching {
logger.w { "TLSv1.2 is not supported in this device; falling through TLSv1.1" }
SSLContext.getInstance(TlsVersion.TLS_1_1.javaName)
}.recoverCatching {
logger.w { "TLSv1.1 is not supported in this device; falling through TLSv1.0" }
// should be available in any device; see reference of supported protocols in
// http://developer.android.com/reference/javax/net/ssl/SSLSocket.html
SSLContext.getInstance(TlsVersion.TLS_1_0.javaName)
}.getOrThrow()
}
}

View File

@@ -1,23 +0,0 @@
package gq.kirmanak.mealient.datasource.impl
import gq.kirmanak.mealient.logging.Logger
import okhttp3.OkHttpClient
import retrofit2.Converter.Factory
import retrofit2.Retrofit
import javax.inject.Inject
internal class RetrofitBuilder @Inject constructor(
private val okHttpClient: OkHttpClient,
private val converterFactory: Factory,
private val logger: Logger,
) {
fun buildRetrofit(baseUrl: String): Retrofit {
logger.v { "buildRetrofit() called with: baseUrl = $baseUrl" }
return Retrofit.Builder()
.baseUrl(baseUrl)
.client(okHttpClient)
.addConverterFactory(converterFactory)
.build()
}
}

View File

@@ -0,0 +1,36 @@
package gq.kirmanak.mealient.datasource.impl
import gq.kirmanak.mealient.logging.Logger
import javax.inject.Inject
import javax.net.ssl.SSLContext
import javax.net.ssl.SSLSocketFactory
import javax.net.ssl.TrustManager
internal class SslSocketFactoryFactory @Inject constructor(
private val advancedX509TrustManager: AdvancedX509TrustManager,
private val logger: Logger,
) {
fun create(): SSLSocketFactory {
val sslContext = buildSSLContext()
sslContext.init(null, arrayOf<TrustManager>(advancedX509TrustManager), null)
return sslContext.socketFactory
}
private fun buildSSLContext(): SSLContext {
return runCatching {
SSLContext.getInstance("TLSv1.3")
}.recoverCatching {
logger.w { "TLSv1.3 is not supported in this device; falling through TLSv1.2" }
SSLContext.getInstance("TLSv1.2")
}.recoverCatching {
logger.w { "TLSv1.2 is not supported in this device; falling through TLSv1.1" }
SSLContext.getInstance("TLSv1.1")
}.recoverCatching {
logger.w { "TLSv1.1 is not supported in this device; falling through TLSv1.0" }
// should be available in any device; see reference of supported protocols in
// http://developer.android.com/reference/javax/net/ssl/SSLSocket.html
SSLContext.getInstance("TLSv1")
}.getOrThrow()
}
}

View File

@@ -0,0 +1,50 @@
package gq.kirmanak.mealient.datasource.ktor
import gq.kirmanak.mealient.datasource.AuthenticationProvider
import gq.kirmanak.mealient.logging.Logger
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.HttpClientEngineConfig
import io.ktor.client.plugins.auth.Auth
import io.ktor.client.plugins.auth.providers.BearerTokens
import io.ktor.client.plugins.auth.providers.bearer
import io.ktor.http.HttpStatusCode
import javax.inject.Inject
import javax.inject.Provider
internal class AuthKtorConfiguration @Inject constructor(
private val authenticationProviderProvider: Provider<AuthenticationProvider>,
private val logger: Logger,
) : KtorConfiguration {
private val authenticationProvider: AuthenticationProvider
get() = authenticationProviderProvider.get()
override fun <T : HttpClientEngineConfig> configure(config: HttpClientConfig<T>) {
config.install(Auth) {
bearer {
loadTokens {
getTokens()
}
refreshTokens {
val newTokens = getTokens()
val sameAccessToken = newTokens?.accessToken == oldTokens?.accessToken
if (sameAccessToken && response.status == HttpStatusCode.Unauthorized) {
authenticationProvider.logout()
null
} else {
newTokens
}
}
sendWithoutRequest { true }
}
}
}
private suspend fun getTokens(): BearerTokens? {
val token = authenticationProvider.getAuthToken()
logger.v { "getTokens(): token = $token" }
return token?.let { BearerTokens(accessToken = it, refreshToken = "") }
}
}

View File

@@ -0,0 +1,19 @@
package gq.kirmanak.mealient.datasource.ktor
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.HttpClientEngineConfig
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import javax.inject.Inject
internal class ContentNegotiationConfiguration @Inject constructor(
private val json: Json,
) : KtorConfiguration {
override fun <T : HttpClientEngineConfig> configure(config: HttpClientConfig<T>) {
config.install(ContentNegotiation) {
json(json)
}
}
}

View File

@@ -0,0 +1,17 @@
package gq.kirmanak.mealient.datasource.ktor
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.HttpClientEngineConfig
import io.ktor.client.plugins.compression.ContentEncoding
import javax.inject.Inject
internal class EncodingKtorConfiguration @Inject constructor() : KtorConfiguration {
override fun <T : HttpClientEngineConfig> configure(config: HttpClientConfig<T>) {
config.install(ContentEncoding) {
gzip()
deflate()
identity()
}
}
}

View File

@@ -0,0 +1,8 @@
package gq.kirmanak.mealient.datasource.ktor
import io.ktor.client.HttpClient
internal interface KtorClientBuilder {
fun buildKtorClient(): HttpClient
}

View File

@@ -0,0 +1,32 @@
package gq.kirmanak.mealient.datasource.ktor
import gq.kirmanak.mealient.logging.Logger
import io.ktor.client.HttpClient
import io.ktor.client.engine.okhttp.OkHttp
import okhttp3.OkHttpClient
import javax.inject.Inject
internal class KtorClientBuilderImpl @Inject constructor(
private val configurators: Set<@JvmSuppressWildcards KtorConfiguration>,
private val logger: Logger,
private val okHttpClient: OkHttpClient,
) : KtorClientBuilder {
override fun buildKtorClient(): HttpClient {
logger.v { "buildKtorClient() called" }
val client = HttpClient(OkHttp) {
expectSuccess = true
configurators.forEach {
it.configure(config = this)
}
engine {
preconfigured = okHttpClient
}
}
return client
}
}

View File

@@ -0,0 +1,9 @@
package gq.kirmanak.mealient.datasource.ktor
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.HttpClientEngineConfig
internal interface KtorConfiguration {
fun <T : HttpClientEngineConfig> configure(config: HttpClientConfig<T>)
}

View File

@@ -0,0 +1,41 @@
package gq.kirmanak.mealient.datasource.ktor
import dagger.Binds
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.multibindings.IntoSet
import gq.kirmanak.mealient.datasource.SignOutHandler
import io.ktor.client.HttpClient
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
internal interface KtorModule {
companion object {
@Provides
@Singleton
fun provideClient(builder: KtorClientBuilder): HttpClient = builder.buildKtorClient()
}
@Binds
@IntoSet
fun bindAuthKtorConfiguration(impl: AuthKtorConfiguration) : KtorConfiguration
@Binds
@IntoSet
fun bindEncodingKtorConfiguration(impl: EncodingKtorConfiguration) : KtorConfiguration
@Binds
@IntoSet
fun bindContentNegotiationConfiguration(impl: ContentNegotiationConfiguration) : KtorConfiguration
@Binds
fun bindKtorClientBuilder(impl: KtorClientBuilderImpl) : KtorClientBuilder
@Binds
fun bindSignOutHandler(impl: SignOutHandlerKtor) : SignOutHandler
}

View File

@@ -0,0 +1,20 @@
package gq.kirmanak.mealient.datasource.ktor
import gq.kirmanak.mealient.datasource.SignOutHandler
import io.ktor.client.HttpClient
import io.ktor.client.plugins.auth.Auth
import io.ktor.client.plugins.auth.providers.BearerAuthProvider
import io.ktor.client.plugins.plugin
import javax.inject.Inject
internal class SignOutHandlerKtor @Inject constructor(
private val httpClient: HttpClient,
) : SignOutHandler {
override fun signOut() {
httpClient.plugin(Auth)
.providers
.filterIsInstance<BearerAuthProvider>()
.forEach { it.clearToken() }
}
}

View File

@@ -1,9 +1,9 @@
package gq.kirmanak.mealient.datasource.v1.models
package gq.kirmanak.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CreateRecipeRequestV1(
data class CreateApiTokenRequest(
@SerialName("name") val name: String,
)

View File

@@ -1,9 +1,9 @@
package gq.kirmanak.mealient.datasource.v1.models
package gq.kirmanak.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CreateApiTokenResponseV1(
data class CreateApiTokenResponse(
@SerialName("token") val token: String,
)

View File

@@ -1,9 +1,9 @@
package gq.kirmanak.mealient.datasource.v1.models
package gq.kirmanak.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CreateApiTokenRequestV1(
data class CreateRecipeRequest(
@SerialName("name") val name: String,
)

View File

@@ -1,10 +1,10 @@
package gq.kirmanak.mealient.datasource.v1.models
package gq.kirmanak.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CreateShoppingListItemRequestV1(
data class CreateShoppingListItemRequest(
@SerialName("shopping_list_id") val shoppingListId: String,
@SerialName("checked") val checked: Boolean,
@SerialName("position") val position: Int?,

View File

@@ -1,9 +1,9 @@
package gq.kirmanak.mealient.datasource.v1.models
package gq.kirmanak.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ErrorDetailV1(
data class ErrorDetail(
@SerialName("detail") val detail: String? = null,
)

View File

@@ -1,6 +0,0 @@
package gq.kirmanak.mealient.datasource.models
data class FoodInfo(
val name: String,
val id: String
)

View File

@@ -1,26 +0,0 @@
package gq.kirmanak.mealient.datasource.models
data class FullRecipeInfo(
val remoteId: String,
val name: String,
val recipeYield: String,
val recipeIngredients: List<RecipeIngredientInfo>,
val recipeInstructions: List<RecipeInstructionInfo>,
val settings: RecipeSettingsInfo,
)
data class RecipeSettingsInfo(
val disableAmounts: Boolean,
)
data class RecipeIngredientInfo(
val note: String,
val quantity: Double?,
val unit: String?,
val food: String?,
val title: String?,
)
data class RecipeInstructionInfo(
val text: String,
)

View File

@@ -1,28 +0,0 @@
package gq.kirmanak.mealient.datasource.models
data class FullShoppingListInfo(
val id: String,
val name: String,
val items: List<ShoppingListItemInfo>,
)
data class ShoppingListItemInfo(
val shoppingListId: String,
val id: String,
val checked: Boolean,
val position: Int,
val isFood: Boolean,
val note: String,
val quantity: Double,
val unit: UnitInfo?,
val food: FoodInfo?,
val recipeReferences: List<ShoppingListItemRecipeReferenceInfo>,
)
data class ShoppingListItemRecipeReferenceInfo(
val recipeId: String,
val recipeQuantity: Double,
val id: String,
val shoppingListId: String,
val recipe: FullRecipeInfo,
)

View File

@@ -1,15 +1,15 @@
package gq.kirmanak.mealient.datasource.v1.models
package gq.kirmanak.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetFoodsResponseV1(
@SerialName("items") val items: List<GetFoodResponseV1>,
data class GetFoodsResponse(
@SerialName("items") val items: List<GetFoodResponse>,
)
@Serializable
data class GetFoodResponseV1(
data class GetFoodResponse(
@SerialName("name") val name: String,
@SerialName("id") val id: String,
)

View File

@@ -1,33 +1,33 @@
package gq.kirmanak.mealient.datasource.v1.models
package gq.kirmanak.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetRecipeResponseV1(
data class GetRecipeResponse(
@SerialName("id") val remoteId: String,
@SerialName("name") val name: String,
@SerialName("recipeYield") val recipeYield: String = "",
@SerialName("recipeIngredient") val recipeIngredients: List<GetRecipeIngredientResponseV1> = emptyList(),
@SerialName("recipeInstructions") val recipeInstructions: List<GetRecipeInstructionResponseV1> = emptyList(),
@SerialName("settings") val settings: GetRecipeSettingsResponseV1? = null,
@SerialName("recipeIngredient") val recipeIngredients: List<GetRecipeIngredientResponse> = emptyList(),
@SerialName("recipeInstructions") val recipeInstructions: List<GetRecipeInstructionResponse> = emptyList(),
@SerialName("settings") val settings: GetRecipeSettingsResponse? = null,
)
@Serializable
data class GetRecipeSettingsResponseV1(
data class GetRecipeSettingsResponse(
@SerialName("disableAmount") val disableAmount: Boolean,
)
@Serializable
data class GetRecipeIngredientResponseV1(
data class GetRecipeIngredientResponse(
@SerialName("note") val note: String = "",
@SerialName("unit") val unit: GetRecipeIngredientUnitResponseV1?,
@SerialName("food") val food: GetRecipeIngredientFoodResponseV1?,
@SerialName("unit") val unit: GetUnitResponse?,
@SerialName("food") val food: GetFoodResponse?,
@SerialName("quantity") val quantity: Double?,
@SerialName("title") val title: String?,
)
@Serializable
data class GetRecipeInstructionResponseV1(
data class GetRecipeInstructionResponse(
@SerialName("text") val text: String,
)

View File

@@ -1,4 +1,4 @@
package gq.kirmanak.mealient.datasource.v1.models
package gq.kirmanak.mealient.datasource.models
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
@@ -6,7 +6,7 @@ import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetRecipeSummaryResponseV1(
data class GetRecipeSummaryResponse(
@SerialName("id") val remoteId: String,
@SerialName("name") val name: String,
@SerialName("slug") val slug: String,

View File

@@ -1,9 +1,9 @@
package gq.kirmanak.mealient.datasource.v1.models
package gq.kirmanak.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetRecipesResponseV1(
@SerialName("items") val items: List<GetRecipeSummaryResponseV1>,
data class GetRecipesResponse(
@SerialName("items") val items: List<GetRecipeSummaryResponse>,
)

View File

@@ -1,19 +1,19 @@
package gq.kirmanak.mealient.datasource.v1.models
package gq.kirmanak.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetShoppingListResponseV1(
data class GetShoppingListResponse(
@SerialName("id") val id: String,
@SerialName("groupId") val groupId: String,
@SerialName("name") val name: String = "",
@SerialName("listItems") val listItems: List<GetShoppingListItemResponseV1> = emptyList(),
@SerialName("recipeReferences") val recipeReferences: List<GetShoppingListItemRecipeReferenceFullResponseV1>,
@SerialName("listItems") val listItems: List<GetShoppingListItemResponse> = emptyList(),
@SerialName("recipeReferences") val recipeReferences: List<GetShoppingListItemRecipeReferenceFullResponse>,
)
@Serializable
data class GetShoppingListItemResponseV1(
data class GetShoppingListItemResponse(
@SerialName("shoppingListId") val shoppingListId: String,
@SerialName("id") val id: String,
@SerialName("checked") val checked: Boolean = false,
@@ -21,22 +21,22 @@ data class GetShoppingListItemResponseV1(
@SerialName("isFood") val isFood: Boolean = false,
@SerialName("note") val note: String = "",
@SerialName("quantity") val quantity: Double = 0.0,
@SerialName("unit") val unit: GetRecipeIngredientUnitResponseV1? = null,
@SerialName("food") val food: GetRecipeIngredientFoodResponseV1? = null,
@SerialName("recipeReferences") val recipeReferences: List<GetShoppingListItemRecipeReferenceResponseV1> = emptyList(),
@SerialName("unit") val unit: GetUnitResponse? = null,
@SerialName("food") val food: GetFoodResponse? = null,
@SerialName("recipeReferences") val recipeReferences: List<GetShoppingListItemRecipeReferenceResponse> = emptyList(),
)
@Serializable
data class GetShoppingListItemRecipeReferenceResponseV1(
data class GetShoppingListItemRecipeReferenceResponse(
@SerialName("recipeId") val recipeId: String,
@SerialName("recipeQuantity") val recipeQuantity: Double = 0.0
)
@Serializable
data class GetShoppingListItemRecipeReferenceFullResponseV1(
data class GetShoppingListItemRecipeReferenceFullResponse(
@SerialName("id") val id: String,
@SerialName("shoppingListId") val shoppingListId: String,
@SerialName("recipeId") val recipeId: String,
@SerialName("recipeQuantity") val recipeQuantity: Double = 0.0,
@SerialName("recipe") val recipe: GetRecipeResponseV1,
@SerialName("recipe") val recipe: GetRecipeResponse,
)

View File

@@ -1,13 +1,13 @@
package gq.kirmanak.mealient.datasource.v1.models
package gq.kirmanak.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetShoppingListsResponseV1(
data class GetShoppingListsResponse(
@SerialName("page") val page: Int,
@SerialName("per_page") val perPage: Int,
@SerialName("total") val total: Int,
@SerialName("total_pages") val totalPages: Int,
@SerialName("items") val items: List<GetShoppingListsSummaryResponseV1>,
@SerialName("items") val items: List<GetShoppingListsSummaryResponse>,
)

View File

@@ -1,10 +1,10 @@
package gq.kirmanak.mealient.datasource.v1.models
package gq.kirmanak.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetShoppingListsSummaryResponseV1(
data class GetShoppingListsSummaryResponse(
@SerialName("id") val id: String,
@SerialName("name") val name: String?,
)

View File

@@ -1,9 +1,9 @@
package gq.kirmanak.mealient.datasource.v1.models
package gq.kirmanak.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetTokenResponseV1(
data class GetTokenResponse(
@SerialName("access_token") val accessToken: String,
)

View File

@@ -1,15 +1,15 @@
package gq.kirmanak.mealient.datasource.v1.models
package gq.kirmanak.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetUnitsResponseV1(
@SerialName("items") val items: List<GetUnitResponseV1>
data class GetUnitsResponse(
@SerialName("items") val items: List<GetUnitResponse>
)
@Serializable
data class GetUnitResponseV1(
data class GetUnitResponse(
@SerialName("name") val name: String,
@SerialName("id") val id: String
)

View File

@@ -1,10 +1,10 @@
package gq.kirmanak.mealient.datasource.v1.models
package gq.kirmanak.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetUserInfoResponseV1(
data class GetUserInfoResponse(
@SerialName("id") val id: String,
@SerialName("favoriteRecipes") val favoriteRecipes: List<String> = emptyList(),
)

View File

@@ -1,11 +0,0 @@
package gq.kirmanak.mealient.datasource.models
data class NewShoppingListItemInfo(
val shoppingListId: String,
val isFood: Boolean,
val note: String,
val quantity: Double,
val unit: UnitInfo?,
val food: FoodInfo?,
val position: Int,
)

View File

@@ -1,6 +0,0 @@
package gq.kirmanak.mealient.datasource.models
data class ParseRecipeURLInfo(
val url: String,
val includeTags: Boolean
)

View File

@@ -1,10 +1,10 @@
package gq.kirmanak.mealient.datasource.v1.models
package gq.kirmanak.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ParseRecipeURLRequestV1(
data class ParseRecipeURLRequest(
@SerialName("url") val url: String,
@SerialName("includeTags") val includeTags: Boolean
)

View File

@@ -1,14 +0,0 @@
package gq.kirmanak.mealient.datasource.models
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

@@ -1,14 +0,0 @@
package gq.kirmanak.mealient.datasource.models
data class ShoppingListsInfo(
val page: Int,
val perPage: Int,
val totalPages: Int,
val totalItems: Int,
val items: List<ShoppingListInfo>,
)
data class ShoppingListInfo(
val name: String,
val id: String,
)

View File

@@ -1,6 +0,0 @@
package gq.kirmanak.mealient.datasource.models
data class UnitInfo(
val name: String,
val id: String
)

View File

@@ -1,19 +1,19 @@
package gq.kirmanak.mealient.datasource.v1.models
package gq.kirmanak.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class UpdateRecipeRequestV1(
data class UpdateRecipeRequest(
@SerialName("description") val description: String,
@SerialName("recipeYield") val recipeYield: String,
@SerialName("recipeIngredient") val recipeIngredient: List<AddRecipeIngredientV1>,
@SerialName("recipeInstructions") val recipeInstructions: List<AddRecipeInstructionV1>,
@SerialName("settings") val settings: AddRecipeSettingsV1,
@SerialName("recipeIngredient") val recipeIngredient: List<AddRecipeIngredient>,
@SerialName("recipeInstructions") val recipeInstructions: List<AddRecipeInstruction>,
@SerialName("settings") val settings: AddRecipeSettings,
)
@Serializable
data class AddRecipeIngredientV1(
data class AddRecipeIngredient(
@SerialName("referenceId") val id: String,
@SerialName("note") val note: String,
) {
@@ -21,7 +21,7 @@ data class AddRecipeIngredientV1(
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AddRecipeIngredientV1
other as AddRecipeIngredient
if (note != other.note) return false
@@ -34,7 +34,7 @@ data class AddRecipeIngredientV1(
}
@Serializable
data class AddRecipeInstructionV1(
data class AddRecipeInstruction(
@SerialName("id") val id: String,
@SerialName("text") val text: String = "",
@SerialName("ingredientReferences") val ingredientReferences: List<String>,
@@ -43,7 +43,7 @@ data class AddRecipeInstructionV1(
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as AddRecipeInstructionV1
other as AddRecipeInstruction
if (text != other.text) return false
if (ingredientReferences != other.ingredientReferences) return false
@@ -59,7 +59,7 @@ data class AddRecipeInstructionV1(
}
@Serializable
data class AddRecipeSettingsV1(
data class AddRecipeSettings(
@SerialName("disableComments") val disableComments: Boolean,
@SerialName("public") val public: Boolean,
)

View File

@@ -1,5 +0,0 @@
package gq.kirmanak.mealient.datasource.models
data class VersionInfo(
val version: String,
)

View File

@@ -1,9 +1,9 @@
package gq.kirmanak.mealient.datasource.v0.models
package gq.kirmanak.mealient.datasource.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class VersionResponseV0(
data class VersionResponse(
@SerialName("version") val version: String,
)

View File

@@ -1,52 +0,0 @@
package gq.kirmanak.mealient.datasource.v0
import gq.kirmanak.mealient.datasource.v0.models.AddRecipeRequestV0
import gq.kirmanak.mealient.datasource.v0.models.CreateApiTokenRequestV0
import gq.kirmanak.mealient.datasource.v0.models.CreateApiTokenResponseV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0
import gq.kirmanak.mealient.datasource.v0.models.GetUserInfoResponseV0
import gq.kirmanak.mealient.datasource.v0.models.ParseRecipeURLRequestV0
import gq.kirmanak.mealient.datasource.v0.models.VersionResponseV0
interface MealieDataSourceV0 {
suspend fun addRecipe(
recipe: AddRecipeRequestV0,
): String
/**
* Tries to acquire authentication token using the provided credentials
*/
suspend fun authenticate(
username: String,
password: String,
): String
suspend fun getVersionInfo(): VersionResponseV0
suspend fun requestRecipes(
start: Int,
limit: Int,
): List<GetRecipeSummaryResponseV0>
suspend fun requestRecipeInfo(
slug: String,
): GetRecipeResponseV0
suspend fun parseRecipeFromURL(
request: ParseRecipeURLRequestV0,
): String
suspend fun createApiToken(
request: CreateApiTokenRequestV0,
): CreateApiTokenResponseV0
suspend fun requestUserInfo(): GetUserInfoResponseV0
suspend fun removeFavoriteRecipe(userId: Int, recipeSlug: String)
suspend fun addFavoriteRecipe(userId: Int, recipeSlug: String)
suspend fun deleteRecipe(slug: String)
}

View File

@@ -1,125 +0,0 @@
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.AddRecipeRequestV0
import gq.kirmanak.mealient.datasource.v0.models.CreateApiTokenRequestV0
import gq.kirmanak.mealient.datasource.v0.models.CreateApiTokenResponseV0
import gq.kirmanak.mealient.datasource.v0.models.ErrorDetailV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeResponseV0
import gq.kirmanak.mealient.datasource.v0.models.GetRecipeSummaryResponseV0
import gq.kirmanak.mealient.datasource.v0.models.GetUserInfoResponseV0
import gq.kirmanak.mealient.datasource.v0.models.ParseRecipeURLRequestV0
import gq.kirmanak.mealient.datasource.v0.models.VersionResponseV0
import kotlinx.serialization.SerializationException
import kotlinx.serialization.json.Json
import retrofit2.HttpException
import java.net.ConnectException
import java.net.SocketTimeoutException
import javax.inject.Inject
class MealieDataSourceV0Impl @Inject constructor(
private val networkRequestWrapper: NetworkRequestWrapper,
private val service: MealieServiceV0,
private val json: Json,
) : MealieDataSourceV0 {
override suspend fun addRecipe(
recipe: AddRecipeRequestV0,
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.addRecipe(recipe) },
logMethod = { "addRecipe" },
logParameters = { "recipe = $recipe" }
)
override suspend fun authenticate(
username: String,
password: String,
): String = networkRequestWrapper.makeCall(
block = { service.getToken(username, password) },
logMethod = { "authenticate" },
logParameters = { "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(): VersionResponseV0 = networkRequestWrapper.makeCall(
block = { service.getVersion() },
logMethod = { "getVersionInfo" },
).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(
start: Int,
limit: Int,
): List<GetRecipeSummaryResponseV0> = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getRecipeSummary(start, limit) },
logMethod = { "requestRecipes" },
logParameters = { "start = $start, limit = $limit" }
)
override suspend fun requestRecipeInfo(
slug: String,
): GetRecipeResponseV0 = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getRecipe(slug) },
logMethod = { "requestRecipeInfo" },
logParameters = { "slug = $slug" }
)
override suspend fun parseRecipeFromURL(
request: ParseRecipeURLRequestV0
): String = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.createRecipeFromURL(request) },
logMethod = { "parseRecipeFromURL" },
logParameters = { "request = $request" },
)
override suspend fun createApiToken(
request: CreateApiTokenRequestV0,
): CreateApiTokenResponseV0 = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.createApiToken(request) },
logMethod = { "createApiToken" },
logParameters = { "request = $request" }
)
override suspend fun requestUserInfo(): GetUserInfoResponseV0 {
return networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.getUserSelfInfo() },
logMethod = { "requestUserInfo" },
)
}
override suspend fun removeFavoriteRecipe(
userId: Int,
recipeSlug: String
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.removeFavoriteRecipe(userId, recipeSlug) },
logMethod = { "removeFavoriteRecipe" },
logParameters = { "userId = $userId, recipeSlug = $recipeSlug" }
)
override suspend fun addFavoriteRecipe(
userId: Int,
recipeSlug: String
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.addFavoriteRecipe(userId, recipeSlug) },
logMethod = { "addFavoriteRecipe" },
logParameters = { "userId = $userId, recipeSlug = $recipeSlug" }
)
override suspend fun deleteRecipe(
slug: String
): Unit = networkRequestWrapper.makeCallAndHandleUnauthorized(
block = { service.deleteRecipe(slug) },
logMethod = { "deleteRecipe" },
logParameters = { "slug = $slug" }
)
}

View File

@@ -1,63 +0,0 @@
package gq.kirmanak.mealient.datasource.v0
import gq.kirmanak.mealient.datasource.v0.models.*
import retrofit2.http.*
interface MealieServiceV0 {
@FormUrlEncoded
@POST("/api/auth/token")
suspend fun getToken(
@Field("username") username: String,
@Field("password") password: String,
): GetTokenResponseV0
@POST("/api/recipes/create")
suspend fun addRecipe(
@Body addRecipeRequestV0: AddRecipeRequestV0,
): String
@GET("/api/debug/version")
suspend fun getVersion(): VersionResponseV0
@GET("/api/recipes/summary")
suspend fun getRecipeSummary(
@Query("start") start: Int,
@Query("limit") limit: Int,
): List<GetRecipeSummaryResponseV0>
@GET("/api/recipes/{slug}")
suspend fun getRecipe(
@Path("slug") slug: String,
): GetRecipeResponseV0
@POST("/api/recipes/create-url")
suspend fun createRecipeFromURL(
@Body request: ParseRecipeURLRequestV0,
): String
@POST("/api/users/api-tokens")
suspend fun createApiToken(
@Body request: CreateApiTokenRequestV0,
): CreateApiTokenResponseV0
@GET("/api/users/self")
suspend fun getUserSelfInfo(): GetUserInfoResponseV0
@DELETE("/api/users/{userId}/favorites/{recipeSlug}")
suspend fun removeFavoriteRecipe(
@Path("userId") userId: Int,
@Path("recipeSlug") recipeSlug: String
)
@POST("/api/users/{userId}/favorites/{recipeSlug}")
suspend fun addFavoriteRecipe(
@Path("userId") userId: Int,
@Path("recipeSlug") recipeSlug: String
)
@DELETE("/api/recipes/{slug}")
suspend fun deleteRecipe(
@Path("slug") slug: String
)
}

View File

@@ -1,30 +0,0 @@
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

@@ -1,9 +0,0 @@
package gq.kirmanak.mealient.datasource.v0.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CreateApiTokenRequestV0(
@SerialName("name") val name: String,
)

View File

@@ -1,9 +0,0 @@
package gq.kirmanak.mealient.datasource.v0.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class CreateApiTokenResponseV0(
@SerialName("token") val token: String
)

View File

@@ -1,7 +0,0 @@
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

@@ -1,23 +0,0 @@
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

@@ -1,16 +0,0 @@
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

@@ -1,9 +0,0 @@
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

@@ -1,10 +0,0 @@
package gq.kirmanak.mealient.datasource.v0.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetUserInfoResponseV0(
@SerialName("id") val id: Int,
@SerialName("favoriteRecipes") val favoriteRecipes: List<String> = emptyList(),
)

View File

@@ -1,9 +0,0 @@
package gq.kirmanak.mealient.datasource.v0.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class ParseRecipeURLRequestV0(
@SerialName("url") val url: String,
)

View File

@@ -1,79 +0,0 @@
package gq.kirmanak.mealient.datasource.v1
import gq.kirmanak.mealient.datasource.models.ShoppingListItemInfo
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenRequestV1
import gq.kirmanak.mealient.datasource.v1.models.CreateApiTokenResponseV1
import gq.kirmanak.mealient.datasource.v1.models.CreateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.CreateShoppingListItemRequestV1
import gq.kirmanak.mealient.datasource.v1.models.GetFoodsResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetShoppingListsResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetUnitsResponseV1
import gq.kirmanak.mealient.datasource.v1.models.GetUserInfoResponseV1
import gq.kirmanak.mealient.datasource.v1.models.ParseRecipeURLRequestV1
import gq.kirmanak.mealient.datasource.v1.models.UpdateRecipeRequestV1
import gq.kirmanak.mealient.datasource.v1.models.VersionResponseV1
interface MealieDataSourceV1 {
suspend fun createRecipe(
recipe: CreateRecipeRequestV1,
): String
suspend fun updateRecipe(
slug: String,
recipe: UpdateRecipeRequestV1,
): GetRecipeResponseV1
/**
* Tries to acquire authentication token using the provided credentials
*/
suspend fun authenticate(
username: String,
password: String,
): String
suspend fun getVersionInfo(
): VersionResponseV1
suspend fun requestRecipes(
page: Int,
perPage: Int,
): List<GetRecipeSummaryResponseV1>
suspend fun requestRecipeInfo(
slug: String,
): GetRecipeResponseV1
suspend fun parseRecipeFromURL(
request: ParseRecipeURLRequestV1,
): String
suspend fun createApiToken(
request: CreateApiTokenRequestV1,
): CreateApiTokenResponseV1
suspend fun requestUserInfo(): GetUserInfoResponseV1
suspend fun removeFavoriteRecipe(userId: String, recipeSlug: String)
suspend fun addFavoriteRecipe(userId: String, recipeSlug: String)
suspend fun deleteRecipe(slug: String)
suspend fun getShoppingLists(page: Int, perPage: Int): GetShoppingListsResponseV1
suspend fun getShoppingList(id: String): GetShoppingListResponseV1
suspend fun deleteShoppingListItem(id: String)
suspend fun updateShoppingListItem(item: ShoppingListItemInfo)
suspend fun getFoods(): GetFoodsResponseV1
suspend fun getUnits(): GetUnitsResponseV1
suspend fun addShoppingListItem(request: CreateShoppingListItemRequestV1)
}

View File

@@ -1,112 +0,0 @@
package gq.kirmanak.mealient.datasource.v1
import gq.kirmanak.mealient.datasource.v1.models.*
import kotlinx.serialization.json.JsonElement
import retrofit2.http.*
interface MealieServiceV1 {
@FormUrlEncoded
@POST("/api/auth/token")
suspend fun getToken(
@Field("username") username: String,
@Field("password") password: String,
): GetTokenResponseV1
@POST("/api/recipes")
suspend fun createRecipe(
@Body addRecipeRequest: CreateRecipeRequestV1,
): String
@PATCH("/api/recipes/{slug}")
suspend fun updateRecipe(
@Body addRecipeRequest: UpdateRecipeRequestV1,
@Path("slug") slug: String,
): GetRecipeResponseV1
@GET("/api/app/about")
suspend fun getVersion(): VersionResponseV1
@GET("/api/recipes")
suspend fun getRecipeSummary(
@Query("page") page: Int,
@Query("perPage") perPage: Int,
): GetRecipesResponseV1
@GET("/api/recipes/{slug}")
suspend fun getRecipe(
@Path("slug") slug: String,
): GetRecipeResponseV1
@POST("/api/recipes/create-url")
suspend fun createRecipeFromURL(
@Body request: ParseRecipeURLRequestV1,
): String
@POST("/api/users/api-tokens")
suspend fun createApiToken(
@Body request: CreateApiTokenRequestV1,
): CreateApiTokenResponseV1
@GET("/api/users/self")
suspend fun getUserSelfInfo(): GetUserInfoResponseV1
@DELETE("/api/users/{userId}/favorites/{recipeSlug}")
suspend fun removeFavoriteRecipe(
@Path("userId") userId: String,
@Path("recipeSlug") recipeSlug: String
)
@POST("/api/users/{userId}/favorites/{recipeSlug}")
suspend fun addFavoriteRecipe(
@Path("userId") userId: String,
@Path("recipeSlug") recipeSlug: String
)
@DELETE("/api/recipes/{slug}")
suspend fun deleteRecipe(
@Path("slug") slug: String
)
@GET("/api/groups/shopping/lists")
suspend fun getShoppingLists(
@Query("page") page: Int,
@Query("perPage") perPage: Int,
): GetShoppingListsResponseV1
@GET("/api/groups/shopping/lists/{id}")
suspend fun getShoppingList(
@Path("id") id: String,
): GetShoppingListResponseV1
@GET("/api/groups/shopping/items/{id}")
suspend fun getShoppingListItem(
@Path("id") id: String,
): JsonElement
@PUT("/api/groups/shopping/items/{id}")
suspend fun updateShoppingListItem(
@Path("id") id: String,
@Body request: JsonElement,
)
@DELETE("/api/groups/shopping/items/{id}")
suspend fun deleteShoppingListItem(
@Path("id") id: String,
)
@GET("/api/foods")
suspend fun getFoods(
@Query("perPage") perPage: Int,
): GetFoodsResponseV1
@GET("/api/units")
suspend fun getUnits(
@Query("perPage") perPage: Int,
): GetUnitsResponseV1
@POST("/api/groups/shopping/items")
suspend fun createShoppingListItem(
@Body request: CreateShoppingListItemRequestV1,
)
}

View File

@@ -1,10 +0,0 @@
package gq.kirmanak.mealient.datasource.v1.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetRecipeIngredientFoodResponseV1(
@SerialName("name") val name: String = "",
@SerialName("id") val id: String = "",
)

View File

@@ -1,10 +0,0 @@
package gq.kirmanak.mealient.datasource.v1.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class GetRecipeIngredientUnitResponseV1(
@SerialName("name") val name: String = "",
@SerialName("id") val id: String = "",
)

View File

@@ -1,9 +0,0 @@
package gq.kirmanak.mealient.datasource.v1.models
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@Serializable
data class VersionResponseV1(
@SerialName("version") val version: String,
)