diff --git a/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt b/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt index 4bb6a63..381cdef 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/network/MealieDataSourceWrapper.kt @@ -9,8 +9,10 @@ import gq.kirmanak.mealient.data.recipes.network.RecipeDataSource import gq.kirmanak.mealient.datasource.MealieDataSource import gq.kirmanak.mealient.datasource.models.AddRecipeRequest import gq.kirmanak.mealient.datasource.models.GetRecipeResponse -import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse import gq.kirmanak.mealient.datasource.models.NetworkError +import gq.kirmanak.mealient.datasource.v1.MealieDataSourceV1 +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 +import gq.kirmanak.mealient.extensions.runCatchingExceptCancel import gq.kirmanak.mealient.extensions.toVersionInfo import javax.inject.Inject import javax.inject.Singleton @@ -20,28 +22,52 @@ class MealieDataSourceWrapper @Inject constructor( private val baseURLStorage: BaseURLStorage, private val authRepo: AuthRepo, private val mealieDataSource: MealieDataSource, + private val mealieDataSourceV1: MealieDataSourceV1, ) : AddRecipeDataSource, RecipeDataSource, VersionDataSource { override suspend fun addRecipe(recipe: AddRecipeRequest): String = - withAuthHeader { token -> addRecipe(getUrl(), token, recipe) } + withAuthHeader { token -> mealieDataSource.addRecipe(getUrl(), token, recipe) } override suspend fun getVersionInfo(baseUrl: String): VersionInfo = mealieDataSource.getVersionInfo(baseUrl).toVersionInfo() - override suspend fun requestRecipes(start: Int, limit: Int): List = - withAuthHeader { token -> requestRecipes(getUrl(), token, start, limit) } + override suspend fun requestRecipes(start: Int, limit: Int): List = + withAuthHeader { token -> + runCatchingExceptCancel { + mealieDataSource.requestRecipes(getUrl(), token, start, limit).map { + GetRecipeSummaryResponseV1( + remoteId = it.remoteId.toString(), + name = it.name, + slug = it.slug, + image = it.image, + description = it.description, + recipeCategories = it.recipeCategories, + tags = it.tags, + rating = it.rating, + dateAdded = it.dateAdded, + dateUpdated = it.dateUpdated, + ) + } + }.getOrElse { + if (it is NetworkError.NotMealie) { + mealieDataSourceV1.requestRecipes(getUrl(), token, start, limit) + } else { + throw it + } + } + } override suspend fun requestRecipeInfo(slug: String): GetRecipeResponse = - withAuthHeader { token -> requestRecipeInfo(getUrl(), token, slug) } + withAuthHeader { token -> mealieDataSource.requestRecipeInfo(getUrl(), token, slug) } private suspend fun getUrl() = baseURLStorage.requireBaseURL() - private suspend inline fun withAuthHeader(block: MealieDataSource.(String?) -> T): T = - mealieDataSource.runCatching { block(authRepo.getAuthHeader()) }.getOrElse { + private suspend inline fun withAuthHeader(block: (String?) -> T): T = + runCatching { block(authRepo.getAuthHeader()) }.getOrElse { if (it is NetworkError.Unauthorized) { authRepo.invalidateAuthHeader() // Trying again with new authentication header - mealieDataSource.block(authRepo.getAuthHeader()) + block(authRepo.getAuthHeader()) } else { throw it } diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt index a36e84d..3b0d367 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorage.kt @@ -4,14 +4,14 @@ import androidx.paging.PagingSource import gq.kirmanak.mealient.database.recipe.entity.FullRecipeInfo import gq.kirmanak.mealient.database.recipe.entity.RecipeSummaryEntity import gq.kirmanak.mealient.datasource.models.GetRecipeResponse -import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 interface RecipeStorage { - suspend fun saveRecipes(recipes: List) + suspend fun saveRecipes(recipes: List) fun queryRecipes(): PagingSource - suspend fun refreshAll(recipes: List) + suspend fun refreshAll(recipes: List) suspend fun clearAllLocalData() diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt index 702f5e3..ef203d8 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/db/RecipeStorageImpl.kt @@ -6,7 +6,7 @@ import gq.kirmanak.mealient.database.AppDb import gq.kirmanak.mealient.database.recipe.RecipeDao import gq.kirmanak.mealient.database.recipe.entity.* import gq.kirmanak.mealient.datasource.models.GetRecipeResponse -import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 import gq.kirmanak.mealient.extensions.recipeEntity import gq.kirmanak.mealient.extensions.toRecipeEntity import gq.kirmanak.mealient.extensions.toRecipeIngredientEntity @@ -23,7 +23,7 @@ class RecipeStorageImpl @Inject constructor( private val recipeDao: RecipeDao by lazy { db.recipeDao() } override suspend fun saveRecipes( - recipes: List + recipes: List ) = db.withTransaction { logger.v { "saveRecipes() called with $recipes" } @@ -96,7 +96,7 @@ class RecipeStorageImpl @Inject constructor( return recipeDao.queryRecipesByPages() } - override suspend fun refreshAll(recipes: List) { + override suspend fun refreshAll(recipes: List) { logger.v { "refreshAll() called with: recipes = $recipes" } db.withTransaction { recipeDao.removeAllRecipes() diff --git a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt index 5e82886..02e56b9 100644 --- a/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt +++ b/app/src/main/java/gq/kirmanak/mealient/data/recipes/network/RecipeDataSource.kt @@ -1,10 +1,10 @@ package gq.kirmanak.mealient.data.recipes.network import gq.kirmanak.mealient.datasource.models.GetRecipeResponse -import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 interface RecipeDataSource { - suspend fun requestRecipes(start: Int, limit: Int): List + suspend fun requestRecipes(start: Int, limit: Int): List suspend fun requestRecipeInfo(slug: String): GetRecipeResponse } \ No newline at end of file diff --git a/app/src/main/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappings.kt b/app/src/main/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappings.kt index bf13089..1587186 100644 --- a/app/src/main/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappings.kt +++ b/app/src/main/java/gq/kirmanak/mealient/extensions/RemoteToLocalMappings.kt @@ -6,6 +6,7 @@ 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.datasource.v1.models.GetRecipeSummaryResponseV1 import gq.kirmanak.mealient.datastore.recipe.AddRecipeDraft fun GetRecipeResponse.toRecipeEntity() = RecipeEntity( @@ -31,7 +32,7 @@ fun GetRecipeInstructionResponse.toRecipeInstructionEntity(remoteId: String) = text = text ) -fun GetRecipeSummaryResponse.recipeEntity() = RecipeSummaryEntity( +fun GetRecipeSummaryResponseV1.recipeEntity() = RecipeSummaryEntity( remoteId = remoteId, name = name, slug = slug, diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceModule.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceModule.kt index 8d29930..ae9e517 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceModule.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/DataSourceModule.kt @@ -6,6 +6,9 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent +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 @@ -52,6 +55,10 @@ interface DataSourceModule { fun provideMealieService(retrofit: Retrofit): MealieService = retrofit.create() + @Provides + @Singleton + fun provideMealieServiceV1(retrofit: Retrofit): MealieServiceV1 = + retrofit.create() } @Binds @@ -65,4 +72,8 @@ interface DataSourceModule { @Binds @Singleton fun bindMealieDataSource(mealientDataSourceImpl: MealieDataSourceImpl): MealieDataSource + + @Binds + @Singleton + fun bindMealieDataSourceV1(mealientDataSourceImpl: MealieDataSourceV1Impl): MealieDataSourceV1 } \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt index 981bac7..e87c7c9 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceImpl.kt @@ -73,20 +73,7 @@ class MealieDataSourceImpl @Inject constructor( logParameters = { "baseUrl = $baseUrl, token = $token, start = $start, limit = $limit" } ).getOrElse { val code = (it as? HttpException)?.code() ?: throw it - if (code == 404) requestRecipesV1(baseUrl, token, start, limit) else throw it - } - - private suspend fun requestRecipesV1( - baseUrl: String, token: String?, start: Int, limit: Int - ): List { - // Imagine start is 30 and limit is 15. It means that we already have page 1 and 2, now we need page 3 - val perPage = limit - val page = start / perPage + 1 - return makeCall( - block = { getRecipeSummaryV1("$baseUrl/api/recipes", token, page, perPage) }, - logMethod = { "requestRecipesV1" }, - logParameters = { "baseUrl = $baseUrl, token = $token, start = $start, limit = $limit" } - ).map { it.items }.getOrThrowUnauthorized() + if (code == 404) throw NetworkError.NotMealie(it) else throw it } override suspend fun requestRecipeInfo( diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceV1Impl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceV1Impl.kt deleted file mode 100644 index 30a5310..0000000 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceV1Impl.kt +++ /dev/null @@ -1,41 +0,0 @@ -package gq.kirmanak.mealient.datasource - -import gq.kirmanak.mealient.datasource.models.AddRecipeRequest -import gq.kirmanak.mealient.datasource.models.GetRecipeResponse -import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse -import gq.kirmanak.mealient.datasource.models.VersionResponse - -class MealieDataSourceV1Impl : MealieDataSourceV1 { - override suspend fun addRecipe( - baseUrl: String, - token: String?, - recipe: AddRecipeRequest - ): String { - TODO("Not yet implemented") - } - - override suspend fun authenticate(baseUrl: String, username: String, password: String): String { - TODO("Not yet implemented") - } - - override suspend fun getVersionInfo(baseUrl: String): VersionResponse { - TODO("Not yet implemented") - } - - override suspend fun requestRecipes( - baseUrl: String, - token: String?, - start: Int, - limit: Int - ): List { - TODO("Not yet implemented") - } - - override suspend fun requestRecipeInfo( - baseUrl: String, - token: String?, - slug: String - ): GetRecipeResponse { - TODO("Not yet implemented") - } -} \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt index 6067062..9750cf9 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieService.kt @@ -34,14 +34,6 @@ interface MealieService { @Query("limit") limit: Int, ): List - @GET - suspend fun getRecipeSummaryV1( - @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, diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeSummaryResponse.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeSummaryResponse.kt index 828811e..4fe3b80 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeSummaryResponse.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipeSummaryResponse.kt @@ -7,7 +7,7 @@ import kotlinx.serialization.Serializable @Serializable data class GetRecipeSummaryResponse( - @SerialName("id") val remoteId: String, + @SerialName("id") val remoteId: Int, @SerialName("name") val name: String, @SerialName("slug") val slug: String, @SerialName("image") val image: String?, diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt similarity index 84% rename from datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceV1.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt index 6125e5d..33fd5a3 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieDataSourceV1.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1.kt @@ -1,9 +1,9 @@ -package gq.kirmanak.mealient.datasource +package gq.kirmanak.mealient.datasource.v1 import gq.kirmanak.mealient.datasource.models.AddRecipeRequest import gq.kirmanak.mealient.datasource.models.GetRecipeResponse -import gq.kirmanak.mealient.datasource.models.GetRecipeSummaryResponse import gq.kirmanak.mealient.datasource.models.VersionResponse +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 interface MealieDataSourceV1 { @@ -31,7 +31,7 @@ interface MealieDataSourceV1 { token: String?, start: Int, limit: Int, - ): List + ): List suspend fun requestRecipeInfo( baseUrl: String, diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt new file mode 100644 index 0000000..39c4113 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieDataSourceV1Impl.kt @@ -0,0 +1,90 @@ +package gq.kirmanak.mealient.datasource.v1 + +import gq.kirmanak.mealient.datasource.models.AddRecipeRequest +import gq.kirmanak.mealient.datasource.models.GetRecipeResponse +import gq.kirmanak.mealient.datasource.models.NetworkError +import gq.kirmanak.mealient.datasource.models.VersionResponse +import gq.kirmanak.mealient.datasource.v1.models.GetRecipeSummaryResponseV1 +import gq.kirmanak.mealient.logging.Logger +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 logger: Logger, + private val mealieService: MealieServiceV1, + private val json: Json, +) : MealieDataSourceV1 { + + override suspend fun addRecipe( + baseUrl: String, + token: String?, + recipe: AddRecipeRequest + ): String { + TODO("Not yet implemented") + } + + override suspend fun authenticate(baseUrl: String, username: String, password: String): String { + TODO("Not yet implemented") + } + + override suspend fun getVersionInfo(baseUrl: String): VersionResponse = makeCall( + block = { 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?, + start: Int, + limit: Int + ): List { + // Imagine start is 30 and limit is 15. It means that we already have page 1 and 2, now we need page 3 + val perPage = limit + val page = start / perPage + 1 + return makeCall( + block = { getRecipeSummary("$baseUrl/api/recipes", token, page, perPage) }, + logMethod = { "requestRecipesV1" }, + logParameters = { "baseUrl = $baseUrl, token = $token, start = $start, limit = $limit" } + ).map { it.items }.getOrThrowUnauthorized() + } + + override suspend fun requestRecipeInfo( + baseUrl: String, + token: String?, + slug: String + ): GetRecipeResponse { + TODO("Not yet implemented") + } + + private suspend inline fun makeCall( + crossinline block: suspend MealieServiceV1.() -> T, + crossinline logMethod: () -> String, + crossinline logParameters: () -> String, + ): Result { + logger.v { "${logMethod()} called with: ${logParameters()}" } + return mealieService.runCatching { block() } + .onFailure { logger.e(it) { "${logMethod()} request failed with: ${logParameters()}" } } + .onSuccess { logger.d { "${logMethod()} request succeeded with ${logParameters()}" } } + } +} + +private fun Result.getOrThrowUnauthorized(): T = getOrElse { + throw if (it is HttpException && it.code() in listOf(401, 403)) { + NetworkError.Unauthorized(it) + } else { + it + } +} diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieServiceV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt similarity index 78% rename from datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieServiceV1.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt index 8e7dcb8..34b0edc 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/MealieServiceV1.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/MealieServiceV1.kt @@ -1,7 +1,8 @@ -package gq.kirmanak.mealient.datasource +package gq.kirmanak.mealient.datasource.v1 import gq.kirmanak.mealient.datasource.DataSourceModule.Companion.AUTHORIZATION_HEADER_NAME import gq.kirmanak.mealient.datasource.models.* +import gq.kirmanak.mealient.datasource.v1.models.GetRecipesResponseV1 import retrofit2.http.* interface MealieServiceV1 { @@ -28,14 +29,6 @@ interface MealieServiceV1 { @GET suspend fun getRecipeSummary( - @Url url: String, - @Header(AUTHORIZATION_HEADER_NAME) token: String?, - @Query("start") start: Int, - @Query("limit") limit: Int, - ): List - - @GET - suspend fun getRecipeSummaryV1( @Url url: String, @Header(AUTHORIZATION_HEADER_NAME) token: String?, @Query("page") page: Int, diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeSummaryResponseV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeSummaryResponseV1.kt new file mode 100644 index 0000000..3ec4a06 --- /dev/null +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipeSummaryResponseV1.kt @@ -0,0 +1,24 @@ +package gq.kirmanak.mealient.datasource.v1.models + +import kotlinx.datetime.LocalDate +import kotlinx.datetime.LocalDateTime +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +@Serializable +data class GetRecipeSummaryResponseV1( + @SerialName("id") val remoteId: String, + @SerialName("name") val name: String, + @SerialName("slug") val slug: String, + @SerialName("image") val image: String?, + @SerialName("description") val description: String = "", + @SerialName("recipeCategory") val recipeCategories: List, + @SerialName("tags") val tags: List, + @SerialName("rating") val rating: Int?, + @SerialName("dateAdded") val dateAdded: LocalDate, + @SerialName("dateUpdated") val dateUpdated: LocalDateTime +) { + override fun toString(): String { + return "GetRecipeSummaryResponseV1(remoteId=$remoteId, name='$name')" + } +} \ No newline at end of file diff --git a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipesResponseV1.kt b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipesResponseV1.kt similarity index 77% rename from datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipesResponseV1.kt rename to datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipesResponseV1.kt index 5977b74..08105d4 100644 --- a/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/models/GetRecipesResponseV1.kt +++ b/datasource/src/main/kotlin/gq/kirmanak/mealient/datasource/v1/models/GetRecipesResponseV1.kt @@ -1,9 +1,9 @@ -package gq.kirmanak.mealient.datasource.models +package gq.kirmanak.mealient.datasource.v1.models import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @Serializable data class GetRecipesResponseV1( - @SerialName("items") val items: List, + @SerialName("items") val items: List, ) \ No newline at end of file