From a71c91134c1d1defbf014761f400a846b3f53753 Mon Sep 17 00:00:00 2001 From: dadachi Date: Fri, 27 Mar 2026 19:14:19 +0900 Subject: [PATCH] Extract duplicated API error handling into shared extensions - Add ApiResponseExtensions.kt with reusable emitApiResponse() and throwApiError() functions in the network package - Replace duplicated suspendOnSuccess/suspendOnFailure error handling boilerplate across all 5 repository implementations - Net reduction of ~360 lines of code (+160 -522) Co-Authored-By: Claude Opus 4.6 (1M context) --- .../data/item_tag/ItemTagRepositoryImpl.kt | 165 +----------------- .../login/AccountPasswordRepositoryImpl.kt | 27 +-- .../data/login/LoginRepositoryImpl.kt | 93 +--------- .../data/login/SignUpRepositoryImpl.kt | 125 +------------ .../data/shop/ShopRepositoryImpl.kt | 142 +-------------- .../network/ApiResponseExtensions.kt | 60 +++++++ .../network/ApiResponseExtensionsTest.kt | 70 ++++++++ 7 files changed, 160 insertions(+), 522 deletions(-) create mode 100644 app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensions.kt create mode 100644 app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensionsTest.kt diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/item_tag/ItemTagRepositoryImpl.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/item_tag/ItemTagRepositoryImpl.kt index 05f23bb..ceb9ff0 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/item_tag/ItemTagRepositoryImpl.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/item_tag/ItemTagRepositoryImpl.kt @@ -4,10 +4,7 @@ import com.nativeapptemplate.nativeapptemplatefree.datastore.NatPreferencesDataS import com.nativeapptemplate.nativeapptemplatefree.model.* import com.nativeapptemplate.nativeapptemplatefree.network.Dispatcher import com.nativeapptemplate.nativeapptemplatefree.network.NatDispatchers -import com.skydoves.sandwich.message -import com.skydoves.sandwich.retrofit.serialization.deserializeErrorBody -import com.skydoves.sandwich.suspendOnFailure -import com.skydoves.sandwich.suspendOnSuccess +import com.nativeapptemplate.nativeapptemplatefree.network.emitApiResponse import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow @@ -27,27 +24,7 @@ class ItemTagRepositoryImpl @Inject constructor( mtcPreferencesDataSource.userData.first().accountId, shopId, ) - - response.suspendOnSuccess { - emit(data) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) }.flowOn(ioDispatcher) override fun getItemTag( @@ -57,177 +34,51 @@ class ItemTagRepositoryImpl @Inject constructor( mtcPreferencesDataSource.userData.first().accountId, id, ) - - response.suspendOnSuccess { - emit(data) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) }.flowOn(ioDispatcher) override fun createItemTag( shopId: String, itemTagBody: ItemTagBody, ) = flow { - var itemTag: ItemTag - val response = api.createItemTag( mtcPreferencesDataSource.userData.first().accountId, shopId, itemTagBody, ) - - response.suspendOnSuccess { - itemTag = data - emit(itemTag) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) }.flowOn(ioDispatcher) override fun updateItemTag( id: String, itemTagBody: ItemTagBody, ) = flow { - var itemTag: ItemTag - val response = api.updateItemTag( mtcPreferencesDataSource.userData.first().accountId, id, itemTagBody, ) - - response.suspendOnSuccess { - itemTag = data - emit(itemTag) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) }.flowOn(ioDispatcher) override fun deleteItemTag( id: String, ) = flow { val response = api.deleteItemTag(mtcPreferencesDataSource.userData.first().accountId, id) - - response.suspendOnSuccess { - emit(true) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) { true } }.flowOn(ioDispatcher) override fun completeItemTag( id: String, ) = flow { val response = api.completeItemTag(mtcPreferencesDataSource.userData.first().accountId, id) - - response.suspendOnSuccess { - emit(data) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) }.flowOn(ioDispatcher) override fun resetItemTag( id: String, ) = flow { val response = api.resetItemTag(mtcPreferencesDataSource.userData.first().accountId, id) - - response.suspendOnSuccess { - emit(data) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) }.flowOn(ioDispatcher) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/login/AccountPasswordRepositoryImpl.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/login/AccountPasswordRepositoryImpl.kt index 1e9a7ea..a7c473a 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/login/AccountPasswordRepositoryImpl.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/login/AccountPasswordRepositoryImpl.kt @@ -4,10 +4,7 @@ import com.nativeapptemplate.nativeapptemplatefree.datastore.NatPreferencesDataS import com.nativeapptemplate.nativeapptemplatefree.model.* import com.nativeapptemplate.nativeapptemplatefree.network.Dispatcher import com.nativeapptemplate.nativeapptemplatefree.network.NatDispatchers -import com.skydoves.sandwich.message -import com.skydoves.sandwich.retrofit.serialization.deserializeErrorBody -import com.skydoves.sandwich.suspendOnFailure -import com.skydoves.sandwich.suspendOnSuccess +import com.nativeapptemplate.nativeapptemplatefree.network.emitApiResponse import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow @@ -26,26 +23,6 @@ class AccountPasswordRepositoryImpl @Inject constructor( natPreferencesDataSource.userData.first().accountId, updatePasswordBody, ) - - response.suspendOnSuccess { - emit(true) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) { true } }.flowOn(ioDispatcher) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/login/LoginRepositoryImpl.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/login/LoginRepositoryImpl.kt index 58121a3..b42b391 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/login/LoginRepositoryImpl.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/login/LoginRepositoryImpl.kt @@ -7,8 +7,8 @@ import com.nativeapptemplate.nativeapptemplatefree.model.LoggedInShopkeeper import com.nativeapptemplate.nativeapptemplatefree.model.Login import com.nativeapptemplate.nativeapptemplatefree.network.Dispatcher import com.nativeapptemplate.nativeapptemplatefree.network.NatDispatchers +import com.nativeapptemplate.nativeapptemplatefree.network.emitApiResponse import com.skydoves.sandwich.message -import com.skydoves.sandwich.retrofit.serialization.deserializeErrorBody import com.skydoves.sandwich.suspendOnFailure import com.skydoves.sandwich.suspendOnSuccess import kotlinx.coroutines.CoroutineDispatcher @@ -31,31 +31,8 @@ class LoginRepositoryImpl @Inject constructor( override fun login( login: Login, ) = flow { - var loggedInShopkeeper: LoggedInShopkeeper - val response = api.login(login) - - response.suspendOnSuccess { - loggedInShopkeeper = data - emit(loggedInShopkeeper) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) }.flowOn(ioDispatcher) override val userData: Flow = @@ -77,81 +54,21 @@ class LoginRepositoryImpl @Inject constructor( val response = api.getPermissions( userData.first().accountId, ) - - response.suspendOnSuccess { - emit(data) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) }.flowOn(ioDispatcher) override fun updateConfirmedPrivacyVersion() = flow { val response = api.updateConfirmedPrivacyVersion( natPreferencesDataSource.userData.first().accountId, ) - - response.suspendOnSuccess { - emit(true) - }.suspendOnFailure { // handles the all error cases from the API request fails. - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) { true } }.flowOn(ioDispatcher) override fun updateConfirmedTermsVersion() = flow { val response = api.updateConfirmedTermsVersion( natPreferencesDataSource.userData.first().accountId, ) - - response.suspendOnSuccess { - emit(true) - }.suspendOnFailure { // handles the all error cases from the API request fails. - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) { true } }.flowOn(ioDispatcher) override suspend fun setShouldFetchItemTagForShowTagInfoScan(shouldFetchItemTagForShowTagInfoScan: Boolean) { diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/login/SignUpRepositoryImpl.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/login/SignUpRepositoryImpl.kt index 33c9d87..c3616ae 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/login/SignUpRepositoryImpl.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/login/SignUpRepositoryImpl.kt @@ -1,7 +1,5 @@ package com.nativeapptemplate.nativeapptemplatefree.data.login -import com.nativeapptemplate.nativeapptemplatefree.model.LoggedInShopkeeper -import com.nativeapptemplate.nativeapptemplatefree.model.NativeAppTemplateApiError import com.nativeapptemplate.nativeapptemplatefree.model.SendConfirmation import com.nativeapptemplate.nativeapptemplatefree.model.SendResetPassword import com.nativeapptemplate.nativeapptemplatefree.model.SignUp @@ -9,10 +7,7 @@ import com.nativeapptemplate.nativeapptemplatefree.model.SignUpForUpdate import com.nativeapptemplate.nativeapptemplatefree.model.Status import com.nativeapptemplate.nativeapptemplatefree.network.Dispatcher import com.nativeapptemplate.nativeapptemplatefree.network.NatDispatchers -import com.skydoves.sandwich.message -import com.skydoves.sandwich.retrofit.serialization.deserializeErrorBody -import com.skydoves.sandwich.suspendOnFailure -import com.skydoves.sandwich.suspendOnSuccess +import com.nativeapptemplate.nativeapptemplatefree.network.emitApiResponse import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.flow import kotlinx.coroutines.flow.flowOn @@ -25,137 +20,33 @@ class SignUpRepositoryImpl @Inject constructor( override fun signUp( signUp: SignUp, ) = flow { - val response = api.signUp( - signUp, - ) - - response.suspendOnSuccess { - emit(data) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + val response = api.signUp(signUp) + emitApiResponse(response) }.flowOn(ioDispatcher) override fun updateAccount( signUpForUpdate: SignUpForUpdate, ) = flow { - val response = api.updateAccount( - signUpForUpdate, - ) - - response.suspendOnSuccess { - emit(data) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + val response = api.updateAccount(signUpForUpdate) + emitApiResponse(response) }.flowOn(ioDispatcher) override fun deleteAccount() = flow { val response = api.deleteAccount() - - response.suspendOnSuccess { - emit(true) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) { true } }.flowOn(ioDispatcher) override fun sendResetPasswordInstruction( sendResetPassword: SendResetPassword, ) = flow { val response = api.sendResetPasswordInstruction(sendResetPassword) - - response.suspendOnSuccess { - emit(true) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) { true } }.flowOn(ioDispatcher) override fun sendConfirmationInstruction( sendConfirmation: SendConfirmation, ) = flow { val response = api.sendConfirmationInstruction(sendConfirmation) - - response.suspendOnSuccess { - emit(true) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) { true } }.flowOn(ioDispatcher) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/shop/ShopRepositoryImpl.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/shop/ShopRepositoryImpl.kt index 36e825b..4e38253 100644 --- a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/shop/ShopRepositoryImpl.kt +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/data/shop/ShopRepositoryImpl.kt @@ -4,10 +4,7 @@ import com.nativeapptemplate.nativeapptemplatefree.datastore.NatPreferencesDataS import com.nativeapptemplate.nativeapptemplatefree.model.* import com.nativeapptemplate.nativeapptemplatefree.network.Dispatcher import com.nativeapptemplate.nativeapptemplatefree.network.NatDispatchers -import com.skydoves.sandwich.message -import com.skydoves.sandwich.retrofit.serialization.deserializeErrorBody -import com.skydoves.sandwich.suspendOnFailure -import com.skydoves.sandwich.suspendOnSuccess +import com.nativeapptemplate.nativeapptemplatefree.network.emitApiResponse import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flow @@ -24,27 +21,7 @@ class ShopRepositoryImpl @Inject constructor( val response = api.getShops( natPreferencesDataSource.userData.first().accountId, ) - - response.suspendOnSuccess { - emit(data) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) }.flowOn(ioDispatcher) override fun getShop( @@ -54,147 +31,42 @@ class ShopRepositoryImpl @Inject constructor( natPreferencesDataSource.userData.first().accountId, id, ) - response.suspendOnSuccess { - emit(data) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) }.flowOn(ioDispatcher) override fun createShop( shopBody: ShopBody, ) = flow { - var shop: Shop - val response = api.createShop( natPreferencesDataSource.userData.first().accountId, shopBody, ) - - response.suspendOnSuccess { - shop = data - emit(shop) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) }.flowOn(ioDispatcher) override fun updateShop( id: String, shopUpdateBody: ShopUpdateBody, ) = flow { - var shop: Shop - val response = api.updateShop( natPreferencesDataSource.userData.first().accountId, id, shopUpdateBody, ) - - response.suspendOnSuccess { - shop = data - emit(shop) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) }.flowOn(ioDispatcher) override fun deleteShop( id: String, ) = flow { val response = api.deleteShop(natPreferencesDataSource.userData.first().accountId, id) - - response.suspendOnSuccess { - emit(true) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) { true } }.flowOn(ioDispatcher) override fun resetShop( id: String, ) = flow { val response = api.resetShop(natPreferencesDataSource.userData.first().accountId, id) - - response.suspendOnSuccess { - emit(true) - }.suspendOnFailure { - val nativeAppTemplateApiError: NativeAppTemplateApiError? - - try { - nativeAppTemplateApiError = response.deserializeErrorBody() - } catch (exception: Exception) { - val message = "Not processable error(${message()})." - throw Exception(message) - } - - if (nativeAppTemplateApiError != null) { - val message = "${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]" - throw Exception(message) - } else { - val message = "Not processable error(${message()})." - throw Exception(message) - } - } + emitApiResponse(response) { true } }.flowOn(ioDispatcher) } diff --git a/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensions.kt b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensions.kt new file mode 100644 index 0000000..5ca5ae1 --- /dev/null +++ b/app/src/main/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensions.kt @@ -0,0 +1,60 @@ +package com.nativeapptemplate.nativeapptemplatefree.network + +import com.nativeapptemplate.nativeapptemplatefree.model.NativeAppTemplateApiError +import com.skydoves.sandwich.ApiResponse +import com.skydoves.sandwich.message +import com.skydoves.sandwich.retrofit.serialization.deserializeErrorBody +import com.skydoves.sandwich.suspendOnFailure +import com.skydoves.sandwich.suspendOnSuccess +import kotlinx.coroutines.flow.FlowCollector + +/** + * Handles an [ApiResponse] by emitting the data on success, + * or throwing an exception with the API error details on failure. + */ +suspend inline fun FlowCollector.emitApiResponse( + response: ApiResponse, +) { + response.suspendOnSuccess { + emit(data) + }.suspendOnFailure { + throwApiError(response, message()) + } +} + +/** + * Handles an [ApiResponse] by emitting a mapped value on success, + * or throwing an exception with the API error details on failure. + * + * Useful for delete/update operations that return a Boolean or other transformed type. + */ +suspend inline fun FlowCollector.emitApiResponse( + response: ApiResponse, + crossinline transform: (T) -> R, +) { + response.suspendOnSuccess { + emit(transform(data)) + }.suspendOnFailure { + throwApiError(response, message()) + } +} + +/** + * Extracts error details from a failed [ApiResponse] and throws an appropriate exception. + */ +inline fun throwApiError( + response: ApiResponse, + errorMessage: String, +): Nothing { + val nativeAppTemplateApiError: NativeAppTemplateApiError? = try { + response.deserializeErrorBody() + } catch (_: Exception) { + null + } + + if (nativeAppTemplateApiError != null) { + throw Exception("${nativeAppTemplateApiError.message} [Status: ${nativeAppTemplateApiError.code}]") + } else { + throw Exception("Not processable error($errorMessage).") + } +} diff --git a/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensionsTest.kt b/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensionsTest.kt new file mode 100644 index 0000000..fa8a6e7 --- /dev/null +++ b/app/src/test/kotlin/com/nativeapptemplate/nativeapptemplatefree/network/ApiResponseExtensionsTest.kt @@ -0,0 +1,70 @@ +package com.nativeapptemplate.nativeapptemplatefree.network + +import com.skydoves.sandwich.ApiResponse +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.flow +import kotlinx.coroutines.test.runTest +import org.junit.Assert.assertEquals +import org.junit.Assert.assertTrue +import org.junit.Test +import kotlin.test.assertFailsWith + +class ApiResponseExtensionsTest { + + @Test + fun emitApiResponse_onSuccess_emitsData() = runTest { + val response = ApiResponse.Success(data = "hello") + val result = flow { emitApiResponse(response) }.first() + assertEquals("hello", result) + } + + @Test + fun emitApiResponse_withTransform_onSuccess_emitsTransformedValue() = runTest { + val response = ApiResponse.Success(data = "hello") + val result = flow { emitApiResponse(response) { true } }.first() + assertTrue(result) + } + + @Test + fun emitApiResponse_onFailure_throwsException() = runTest { + val response = ApiResponse.Failure.Exception(Exception("network error")) + assertFailsWith { + flow { emitApiResponse(response) }.first() + } + } + + @Test + fun emitApiResponse_withTransform_onFailure_throwsException() = runTest { + val response = ApiResponse.Failure.Exception(Exception("network error")) + assertFailsWith { + flow { emitApiResponse(response) { true } }.first() + } + } + + @Test + fun throwApiError_includesErrorMessageInException() { + val response: ApiResponse = ApiResponse.Failure.Exception(Exception("timeout")) + val exception = assertFailsWith { + throwApiError(response, "timeout") + } + assertEquals("Not processable error(timeout).", exception.message) + } + + @Test + fun emitApiResponse_onFailure_exceptionContainsNotProcessableError() = runTest { + val response: ApiResponse = ApiResponse.Failure.Exception(Exception("server error")) + val exception = assertFailsWith { + flow { emitApiResponse(response) }.first() + } + assertTrue(exception.message!!.contains("Not processable error")) + } + + @Test + fun emitApiResponse_withTransform_onFailure_exceptionContainsNotProcessableError() = runTest { + val response: ApiResponse = ApiResponse.Failure.Exception(Exception("server error")) + val exception = assertFailsWith { + flow { emitApiResponse(response) { true } }.first() + } + assertTrue(exception.message!!.contains("Not processable error")) + } +}