diff --git a/README.md b/README.md index a1c8ab72..c7b904b3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,7 @@ # MVI-Coroutines-Flow +## MVI-Coroutines-Flow-Clean-Architecture +## MVI-Coroutines-Flow-Clean-Architecture-ArrowKt +## MVI-Coroutines-Flow-Clean-Architecture-ArrowKt-KoinDI [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) @@ -9,7 +12,7 @@ [![Qodana](https://github.com/Kotlin-Android-Open-Source/MVI-Coroutines-Flow/actions/workflows/qodana.yml/badge.svg)](https://github.com/Kotlin-Android-Open-Source/MVI-Coroutines-Flow/actions/workflows/qodana.yml) [![Validate Gradle Wrapper](https://github.com/Kotlin-Android-Open-Source/MVI-Coroutines-Flow/actions/workflows/gradle-wrapper-validation.yml/badge.svg)](https://github.com/Kotlin-Android-Open-Source/MVI-Coroutines-Flow/actions/workflows/gradle-wrapper-validation.yml) [![API](https://img.shields.io/badge/API-21%2B-brightgreen.svg?style=flat)](https://android-arsenal.com/api?level=21) -[![Kotlin](https://img.shields.io/badge/kotlin-1.7.20-blue.svg?logo=kotlin)](http://kotlinlang.org) +[![Kotlin](https://img.shields.io/badge/kotlin-1.8.10-blue.svg?logo=kotlin)](http://kotlinlang.org) [![Hits](https://hits.seeyoufarm.com/api/count/incr/badge.svg?url=https%3A%2F%2Fgithub.com%2FKotlin-Android-Open-Source%2FMVI-Coroutines-Flow&count_bg=%2379C83D&title_bg=%23555555&icon=&icon_color=%23E7E7E7&title=hits&edge_flat=false)](https://hits.seeyoufarm.com) [![License: MIT](https://img.shields.io/badge/License-MIT-purple.svg)](https://opensource.org/licenses/MIT) [![Gitter](https://badges.gitter.im/Kotlin-Android-Open-Source/community.svg)](https://gitter.im/Kotlin-Android-Open-Source/community?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge) diff --git a/build.gradle.kts b/build.gradle.kts index 6a701040..9074980f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -17,7 +17,7 @@ buildscript { classpath("com.android.tools.build:gradle:7.4.1") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlinVersion") classpath("com.diffplug.spotless:spotless-plugin-gradle:6.15.0") - classpath("dev.ahmedmourad.nocopy:nocopy-gradle-plugin:1.4.0") + classpath("dev.drewhamilton.poko:poko-gradle-plugin:0.11.0") classpath("org.jacoco:org.jacoco.core:0.8.8") classpath("com.vanniktech:gradle-android-junit-jacoco-plugin:0.17.0-SNAPSHOT") classpath("com.github.ben-manes:gradle-versions-plugin:0.45.0") diff --git a/buildSrc/src/main/kotlin/deps.kt b/buildSrc/src/main/kotlin/deps.kt index 4f9ab5bd..22418caf 100644 --- a/buildSrc/src/main/kotlin/deps.kt +++ b/buildSrc/src/main/kotlin/deps.kt @@ -7,16 +7,16 @@ import org.gradle.plugin.use.PluginDependenciesSpec import org.gradle.plugin.use.PluginDependencySpec const val ktlintVersion = "0.46.1" -const val kotlinVersion = "1.7.20" +const val kotlinVersion = "1.8.10" object appConfig { const val applicationId = "com.hoc.flowmvi" - const val compileSdkVersion = 32 - const val buildToolsVersion = "32.0.0" + const val compileSdkVersion = 33 + const val buildToolsVersion = "33.0.1" const val minSdkVersion = 21 - const val targetSdkVersion = 32 + const val targetSdkVersion = 33 private const val MAJOR = 2 private const val MINOR = 1 @@ -28,16 +28,16 @@ object appConfig { object deps { object androidx { const val appCompat = "androidx.appcompat:appcompat:1.4.2" - const val coreKtx = "androidx.core:core-ktx:1.8.0" + const val coreKtx = "androidx.core:core-ktx:1.9.0" const val constraintLayout = "androidx.constraintlayout:constraintlayout:2.1.4" const val recyclerView = "androidx.recyclerview:recyclerview:1.2.1" const val swipeRefreshLayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01" - const val material = "com.google.android.material:material:1.6.1" + const val material = "com.google.android.material:material:1.8.0" const val startup = "androidx.startup:startup-runtime:1.1.1" } object lifecycle { - private const val version = "2.5.0" + private const val version = "2.5.1" const val viewModelKtx = "androidx.lifecycle:lifecycle-viewmodel-ktx:$version" // viewModelScope const val runtimeKtx = "androidx.lifecycle:lifecycle-runtime-ktx:$version" // lifecycleScope @@ -49,7 +49,7 @@ object deps { const val converterMoshi = "com.squareup.retrofit2:converter-moshi:2.9.0" const val loggingInterceptor = "com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.10" const val moshiKotlin = "com.squareup.moshi:moshi-kotlin:1.13.0" - const val leakCanary = "com.squareup.leakcanary:leakcanary-android:2.9.1" + const val leakCanary = "com.squareup.leakcanary:leakcanary-android:2.10" } object coroutines { @@ -61,7 +61,7 @@ object deps { } object koin { - private const val version = "3.2.0" + private const val version = "3.3.3" const val core = "io.insert-koin:koin-core:$version" const val android = "io.insert-koin:koin-android:$version" @@ -75,7 +75,7 @@ object deps { const val timber = "com.jakewharton.timber:timber:5.0.1" object arrow { - private const val version = "1.1.3" + private const val version = "1.1.6-alpha.28" const val core = "io.arrow-kt:arrow-core:$version" } @@ -91,7 +91,7 @@ object deps { } } - const val mockk = "io.mockk:mockk:1.12.4" + const val mockk = "io.mockk:mockk:1.13.4" const val kotlinJUnit = "org.jetbrains.kotlin:kotlin-test-junit:$kotlinVersion" } } @@ -105,7 +105,7 @@ inline val PDsS.kotlinAndroid: PDS get() = id("kotlin-android") inline val PDsS.kotlin: PDS get() = id("kotlin") inline val PDsS.kotlinKapt: PDS get() = id("kotlin-kapt") inline val PDsS.kotlinParcelize: PDS get() = id("kotlin-parcelize") -inline val PDsS.nocopyPlugin: PDS get() = id("dev.ahmedmourad.nocopy.nocopy-gradle-plugin") +inline val PDsS.pokoPlugin: PDS get() = id("dev.drewhamilton.poko") inline val DependencyHandler.domain get() = project(":domain") inline val DependencyHandler.core get() = project(":core") diff --git a/core/src/main/java/com/hoc/flowmvi/core/EitherNes.kt b/core/src/main/java/com/hoc/flowmvi/core/EitherNes.kt new file mode 100644 index 00000000..29c7a099 --- /dev/null +++ b/core/src/main/java/com/hoc/flowmvi/core/EitherNes.kt @@ -0,0 +1,56 @@ +package com.hoc.flowmvi.core + +import arrow.core.Either +import arrow.core.left +import arrow.core.right +import com.hoc.flowmvi.core.NonEmptySet.Companion.toNonEmptySetOrNull +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * A typealias for [Either] with [NonEmptySet] as the left side. + */ +typealias EitherNes = Either, A> + +@Suppress("NOTHING_TO_INLINE") +inline fun A.rightNes(): EitherNes = this.right() + +@Suppress("NOTHING_TO_INLINE") +inline fun E.leftNes(): EitherNes = + NonEmptySet.of(this).left() + +@OptIn(ExperimentalContracts::class) +inline fun Either.Companion.zipOrAccumulateNonEmptySet( + a: EitherNes, + b: EitherNes, + c: EitherNes, + transform: (A, B, C) -> Z, +): EitherNes { + contract { callsInPlace(transform, InvocationKind.AT_MOST_ONCE) } + + return if ( + a is Either.Right && + b is Either.Right && + c is Either.Right + ) { + Either.Right( + transform( + a.value, + b.value, + c.value, + ) + ) + } else { + Either.Left( + buildSet(capacity = a.count + b.count + c.count) { + if (a is Either.Left) this.addAll(a.value) + if (b is Either.Left) this.addAll(b.value) + if (c is Either.Left) this.addAll(c.value) + }.toNonEmptySetOrNull()!! + ) + } +} + +@PublishedApi +internal inline val Either.count: Int get() = if (isRight()) 1 else 0 diff --git a/core/src/main/java/com/hoc/flowmvi/core/ValidatedNes.kt b/core/src/main/java/com/hoc/flowmvi/core/ValidatedNes.kt deleted file mode 100644 index 9ebb52cd..00000000 --- a/core/src/main/java/com/hoc/flowmvi/core/ValidatedNes.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.hoc.flowmvi.core - -import arrow.core.Validated -import arrow.typeclasses.Semigroup - -typealias ValidatedNes = Validated, A> - -@Suppress("NOTHING_TO_INLINE") -inline fun A.validNes(): ValidatedNes = - Validated.Valid(this) - -@Suppress("NOTHING_TO_INLINE") -inline fun E.invalidNes(): ValidatedNes = - Validated.Invalid(NonEmptySet.of(this)) - -object NonEmptySetSemigroup : Semigroup> { - override fun NonEmptySet.combine(b: NonEmptySet): NonEmptySet = this + b -} - -@Suppress("UNCHECKED_CAST") -fun Semigroup.Companion.nonEmptySet(): Semigroup> = - NonEmptySetSemigroup as Semigroup> diff --git a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt index 1d3cc73a..dbee5bcf 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/UserRepositoryImpl.kt @@ -1,13 +1,13 @@ package com.hoc.flowmvi.data import arrow.core.Either.Companion.catch as catchEither -import arrow.core.continuations.either +import arrow.core.getOrElse import arrow.core.left import arrow.core.leftWiden +import arrow.core.raise.either import arrow.core.right -import arrow.core.valueOr +import com.hoc.flowmvi.core.EitherNes import com.hoc.flowmvi.core.Mapper -import com.hoc.flowmvi.core.ValidatedNes import com.hoc.flowmvi.core.dispatchers.AppCoroutineDispatchers import com.hoc.flowmvi.data.remote.UserApiService import com.hoc.flowmvi.data.remote.UserBody @@ -16,6 +16,7 @@ import com.hoc.flowmvi.domain.model.User import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.domain.model.UserValidationError import com.hoc.flowmvi.domain.repository.UserRepository +import com.hoc081098.flowext.flowFromSuspend import com.hoc081098.flowext.retryWithExponentialBackoff import java.io.IOException import kotlin.time.Duration.Companion.milliseconds @@ -24,7 +25,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.FlowPreview import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.flatMapConcat @@ -40,7 +40,7 @@ import timber.log.Timber internal class UserRepositoryImpl( private val userApiService: UserApiService, private val dispatchers: AppCoroutineDispatchers, - private val responseToDomain: Mapper>, + private val responseToDomain: Mapper>, private val domainToBody: Mapper, private val errorMapper: Mapper, ) : UserRepository { @@ -51,29 +51,21 @@ internal class UserRepositoryImpl( class Added(val user: User) : Change() } - private val responseToDomainThrows: (UserResponse) -> User = { response -> - responseToDomain(response).let { validated -> - validated.valueOr { - val t = UserError.ValidationFailed(it.toSet()) - logError(t, "Map $response to user") - throw t - } - } - } - private val changesFlow = MutableSharedFlow(extraBufferCapacity = 64) - private suspend inline fun sendChange(change: Change) = changesFlow.emit(change) - @Suppress("NOTHING_TO_INLINE") - private inline fun logError(t: Throwable, message: String) = Timber.tag(TAG).e(t, message) - - private fun getUsersFromRemote(): Flow> = suspend { + private fun getUsersFromRemote(): Flow> = flowFromSuspend { Timber.d("[USER_REPO] getUsersFromRemote ...") + userApiService .getUsers() - .map(responseToDomainThrows) - }.asFlow() + .map { response -> + responseToDomain(response) + .mapLeft(UserError::ValidationFailed) + .onLeft { logError(it, "Map $response to user") } + .getOrElse { throw it } + } + } .retryWithExponentialBackoff( maxAttempt = 2, initialDelay = 500.milliseconds, @@ -100,50 +92,65 @@ internal class UserRepositoryImpl( } override suspend fun refresh() = catchEither { getUsersFromRemote().first() } - .tap { sendChange(Change.Refreshed(it)) } + .onRight { sendChange(Change.Refreshed(it)) } .map { } - .tapLeft { logError(it, "refresh") } + .onLeft { logError(it, "refresh") } .mapLeft(errorMapper) - override suspend fun remove(user: User) = either { + override suspend fun remove(user: User) = either { withContext(dispatchers.io) { val response = catchEither { userApiService.remove(user.id) } - .tapLeft { logError(it, "remove user=$user") } + .onLeft { logError(it, "remove user=$user") } .mapLeft(errorMapper) .bind() val deleted = responseToDomain(response) - .mapLeft { UserError.ValidationFailed(it.toSet()) } - .tapInvalid { logError(it, "remove user=$user") } + .mapLeft { UserError.ValidationFailed(it) } + .onLeft { logError(it, "remove user=$user") } .bind() sendChange(Change.Removed(deleted)) } } - override suspend fun add(user: User) = either { + override suspend fun add(user: User) = either { withContext(dispatchers.io) { val response = catchEither { userApiService.add(domainToBody(user)) } - .tapLeft { logError(it, "add user=$user") } + .onLeft { logError(it, "add user=$user") } .mapLeft(errorMapper) .bind() val added = responseToDomain(response) - .mapLeft { UserError.ValidationFailed(it.toSet()) } - .tapInvalid { logError(it, "add user=$user") } + .mapLeft { UserError.ValidationFailed(it) } + .onLeft { logError(it, "add user=$user") } .bind() sendChange(Change.Added(added)) } } - override suspend fun search(query: String) = withContext(dispatchers.io) { - catchEither { userApiService.search(query).map(responseToDomainThrows) } - .tapLeft { logError(it, "search query=$query") } - .mapLeft(errorMapper) + override suspend fun search(query: String) = either { + withContext(dispatchers.io) { + val userResponses = catchEither { userApiService.search(query) } + .onLeft { logError(it, "search query=$query") } + .mapLeft(errorMapper) + .bind() + + val users = userResponses.map { userResponse -> + responseToDomain(userResponse) + .mapLeft(UserError::ValidationFailed) + .onLeft { logError(it, "search query=$query") } + .bind() + } + + users + } } private companion object { + @Suppress("NOTHING_TO_INLINE") + private inline fun logError(t: Throwable, message: String) = Timber.tag(TAG).e(t, message) + private val TAG = UserRepositoryImpl::class.java.simpleName } } diff --git a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserErrorMapper.kt b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserErrorMapper.kt index 71d36900..ef48d351 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserErrorMapper.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserErrorMapper.kt @@ -4,6 +4,7 @@ import arrow.core.nonFatalOrThrow import com.hoc.flowmvi.core.Mapper import com.hoc.flowmvi.data.remote.ErrorResponse import com.hoc.flowmvi.domain.model.UserError +import com.hoc.flowmvi.domain.model.UserValidationError import com.squareup.moshi.JsonAdapter import java.io.IOException import java.net.SocketException @@ -48,7 +49,9 @@ internal class UserErrorMapper(private val errorResponseJsonAdapter: JsonAdapter "internal-error" -> UserError.ServerError "invalid-id" -> UserError.InvalidId(id = errorResponse.data as String) "user-not-found" -> UserError.UserNotFound(id = errorResponse.data as String) - "validation-failed" -> UserError.ValidationFailed(errors = emptySet()) + "validation-failed" -> UserError.ValidationFailed( + errors = UserValidationError.VALUES_SET // TODO(hoc081098): Map validation errors from server response + ) else -> UserError.Unexpected } } diff --git a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapper.kt b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapper.kt index 4d6de239..0201a373 100644 --- a/data/src/main/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapper.kt +++ b/data/src/main/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapper.kt @@ -1,14 +1,14 @@ package com.hoc.flowmvi.data.mapper +import com.hoc.flowmvi.core.EitherNes import com.hoc.flowmvi.core.Mapper -import com.hoc.flowmvi.core.ValidatedNes import com.hoc.flowmvi.data.remote.UserResponse import com.hoc.flowmvi.domain.model.User import com.hoc.flowmvi.domain.model.UserValidationError internal class UserResponseToUserDomainMapper : - Mapper> { - override fun invoke(response: UserResponse): ValidatedNes { + Mapper> { + override fun invoke(response: UserResponse): EitherNes { return User.create( id = response.id, avatar = response.avatar, diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt index ebdc606f..9516cc59 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplRealAPITest.kt @@ -3,7 +3,7 @@ package com.hoc.flowmvi.data import android.util.Log import com.hoc.flowmvi.core.dispatchers.AppCoroutineDispatchers import com.hoc.flowmvi.domain.repository.UserRepository -import com.hoc.flowmvi.test_utils.getOrThrow +import com.hoc.flowmvi.test_utils.rightValueOrThrow import java.time.LocalDateTime import java.time.format.DateTimeFormatter import kotlin.test.Test @@ -61,7 +61,7 @@ class UserRepositoryImplRealAPITest : KoinTest { .getUsers() .first() assertTrue(result.isRight()) - assertTrue(result.getOrThrow.isNotEmpty()) + assertTrue(result.rightValueOrThrow.isNotEmpty()) } Unit } diff --git a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt index 153cb64c..cccb7004 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/UserRepositoryImplTest.kt @@ -1,9 +1,9 @@ package com.hoc.flowmvi.data import arrow.core.Either +import com.hoc.flowmvi.core.EitherNes import com.hoc.flowmvi.core.Mapper -import com.hoc.flowmvi.core.ValidatedNes -import com.hoc.flowmvi.core.validNes +import com.hoc.flowmvi.core.rightNes import com.hoc.flowmvi.data.remote.UserApiService import com.hoc.flowmvi.data.remote.UserBody import com.hoc.flowmvi.data.remote.UserResponse @@ -12,9 +12,8 @@ import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.domain.model.UserValidationError import com.hoc.flowmvi.test_utils.TestAppCoroutineDispatchers import com.hoc.flowmvi.test_utils.TestCoroutineDispatcherRule -import com.hoc.flowmvi.test_utils.getOrThrow -import com.hoc.flowmvi.test_utils.leftOrThrow -import com.hoc.flowmvi.test_utils.valueOrThrow +import com.hoc.flowmvi.test_utils.leftValueOrThrow +import com.hoc.flowmvi.test_utils.rightValueOrThrow import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.coVerify @@ -95,9 +94,9 @@ private val USERS = listOf( lastName = "last", avatar = "avatar3", ), -).map { it.valueOrThrow } +).map { it.rightValueOrThrow } -private val VALID_NES_USERS = USERS.map(User::validNes) +private val VALID_NES_USERS = USERS.map(User::rightNes) @FlowPreview @ExperimentalCoroutinesApi @@ -108,7 +107,7 @@ class UserRepositoryImplTest { private lateinit var repo: UserRepositoryImpl private lateinit var userApiService: UserApiService - private lateinit var responseToDomain: Mapper> + private lateinit var responseToDomain: Mapper> private lateinit var domainToBody: Mapper private lateinit var errorMapper: Mapper @@ -147,7 +146,7 @@ class UserRepositoryImplTest { val result = repo.refresh() assertTrue(result.isRight()) - assertNotNull(result.orNull()) + assertNotNull(result.getOrNull()) coVerify { userApiService.getUsers() } verifySequence { @@ -166,7 +165,7 @@ class UserRepositoryImplTest { val result = repo.refresh() assertTrue(result.isLeft()) - assertEquals(UserError.NetworkError, result.leftOrThrow) + assertEquals(UserError.NetworkError, result.leftValueOrThrow) coVerify(exactly = 3) { userApiService.getUsers() } // retry 2 times verify(exactly = 1) { errorMapper(ofType()) } } @@ -177,12 +176,12 @@ class UserRepositoryImplTest { val userResponse = USER_RESPONSES[0] coEvery { userApiService.remove(user.id) } returns userResponse - every { responseToDomain(userResponse) } returns user.validNes() + every { responseToDomain(userResponse) } returns user.rightNes() val result = repo.remove(user) assertTrue(result.isRight()) - assertNotNull(result.orNull()) + assertNotNull(result.getOrNull()) coVerify { userApiService.remove(user.id) } coVerify { responseToDomain(userResponse) } @@ -197,7 +196,7 @@ class UserRepositoryImplTest { val result = repo.remove(user) assertTrue(result.isLeft()) - assertEquals(UserError.NetworkError, result.leftOrThrow) + assertEquals(UserError.NetworkError, result.leftValueOrThrow) coVerify(exactly = 1) { userApiService.remove(user.id) } verify(exactly = 1) { errorMapper(ofType()) } } @@ -209,12 +208,12 @@ class UserRepositoryImplTest { coEvery { userApiService.add(USER_BODY) } returns userResponse every { domainToBody(user) } returns USER_BODY - every { responseToDomain(userResponse) } returns user.validNes() + every { responseToDomain(userResponse) } returns user.rightNes() val result = repo.add(user) assertTrue(result.isRight()) - assertNotNull(result.orNull()) + assertNotNull(result.getOrNull()) coVerify { userApiService.add(USER_BODY) } verify { domainToBody(user) } @@ -231,7 +230,7 @@ class UserRepositoryImplTest { val result = repo.add(user) assertTrue(result.isLeft()) - assertEquals(UserError.NetworkError, result.leftOrThrow) + assertEquals(UserError.NetworkError, result.leftValueOrThrow) coVerify(exactly = 1) { userApiService.add(USER_BODY) } verify(exactly = 1) { domainToBody(user) } @@ -247,8 +246,8 @@ class UserRepositoryImplTest { val result = repo.search(q) assertTrue(result.isRight()) - assertNotNull(result.orNull()) - assertContentEquals(USERS, result.getOrThrow) + assertNotNull(result.getOrNull()) + assertContentEquals(USERS, result.rightValueOrThrow) coVerify { userApiService.search(q) } coVerifySequence { @@ -267,7 +266,7 @@ class UserRepositoryImplTest { val result = repo.search(q) assertTrue(result.isLeft()) - assertEquals(UserError.NetworkError, result.leftOrThrow) + assertEquals(UserError.NetworkError, result.leftValueOrThrow) coVerify(exactly = 1) { userApiService.search(q) } verify(exactly = 1) { errorMapper(ofType()) } @@ -288,8 +287,8 @@ class UserRepositoryImplTest { assertEquals(1, events.size) val result = events.single() assertTrue(result.isRight()) - assertNotNull(result.orNull()) - assertEquals(USERS, result.getOrThrow) + assertNotNull(result.getOrNull()) + assertEquals(USERS, result.rightValueOrThrow) coVerify { userApiService.getUsers() } verifySequence { @@ -314,8 +313,8 @@ class UserRepositoryImplTest { assertEquals(1, events.size) val result = events.single() assertTrue(result.isLeft()) - assertNull(result.orNull()) - assertEquals(UserError.NetworkError, result.leftOrThrow) + assertNull(result.getOrNull()) + assertEquals(UserError.NetworkError, result.leftValueOrThrow) coVerify(exactly = 3) { userApiService.getUsers() } // retry 2 times. verify(exactly = 1) { errorMapper(ofType()) } @@ -331,7 +330,7 @@ class UserRepositoryImplTest { coEvery { userApiService.remove(user.id) } returns userResponse every { domainToBody(user) } returns USER_BODY USER_RESPONSES.zip(USERS) - .forEach { (r, u) -> every { responseToDomain(r) } returns u.validNes() } + .forEach { (r, u) -> every { responseToDomain(r) } returns u.rightNes() } val events = mutableListOf>>() val job = launch(start = CoroutineStart.UNDISPATCHED) { @@ -343,7 +342,7 @@ class UserRepositoryImplTest { job.cancel() assertContentEquals( - events.map { it.getOrThrow }, + events.map { it.rightValueOrThrow }, listOf( USERS.dropLast(1), USERS, diff --git a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt index c0c9a20c..56586c13 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserDomainToUserBodyMapperTest.kt @@ -2,7 +2,7 @@ package com.hoc.flowmvi.data.mapper import com.hoc.flowmvi.data.remote.UserBody import com.hoc.flowmvi.domain.model.User -import com.hoc.flowmvi.test_utils.valueOrThrow +import com.hoc.flowmvi.test_utils.rightValueOrThrow import kotlin.test.Test import kotlin.test.assertEquals @@ -18,7 +18,7 @@ class UserDomainToUserBodyMapperTest { firstName = "first", lastName = "last", avatar = "avatar", - ).valueOrThrow + ).rightValueOrThrow ) assertEquals( diff --git a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt index 9daeac95..301fec7b 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserErrorMapperTest.kt @@ -1,7 +1,9 @@ package com.hoc.flowmvi.data.mapper +import com.hoc.flowmvi.core.NonEmptySet import com.hoc.flowmvi.data.remote.ErrorResponse import com.hoc.flowmvi.domain.model.UserError +import com.hoc.flowmvi.domain.model.UserValidationError import com.squareup.moshi.Moshi import com.squareup.moshi.adapter import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory @@ -49,8 +51,8 @@ class UserErrorMapperTest { assertEquals(UserError.UserNotFound("1"), errorMapper(UserError.UserNotFound("1"))) assertEquals(UserError.InvalidId("1"), errorMapper(UserError.InvalidId("1"))) assertEquals( - UserError.ValidationFailed(emptySet()), - errorMapper(UserError.ValidationFailed(emptySet())), + UserError.ValidationFailed(NonEmptySet.of(UserValidationError.INVALID_EMAIL_ADDRESS)), + errorMapper(UserError.ValidationFailed(NonEmptySet.of(UserValidationError.INVALID_EMAIL_ADDRESS))), ) assertEquals(UserError.ServerError, errorMapper(UserError.ServerError)) assertEquals(UserError.Unexpected, errorMapper(UserError.Unexpected)) @@ -158,7 +160,7 @@ class UserErrorMapperTest { errorMapper(buildHttpException("user-not-found", id)), ) assertEquals( - UserError.ValidationFailed(emptySet()), + UserError.ValidationFailed(UserValidationError.VALUES_SET), errorMapper(buildHttpException("validation-failed", null)), ) } diff --git a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt index 628a89a5..73f868fd 100644 --- a/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt +++ b/data/src/test/java/com/hoc/flowmvi/data/mapper/UserResponseToUserDomainMapperTest.kt @@ -3,8 +3,8 @@ package com.hoc.flowmvi.data.mapper import com.hoc.flowmvi.data.remote.UserResponse import com.hoc.flowmvi.domain.model.User import com.hoc.flowmvi.domain.model.UserValidationError -import com.hoc.flowmvi.test_utils.invalidValueOrThrow -import com.hoc.flowmvi.test_utils.valueOrThrow +import com.hoc.flowmvi.test_utils.leftValueOrThrow +import com.hoc.flowmvi.test_utils.rightValueOrThrow import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -23,7 +23,7 @@ class UserResponseToUserDomainMapperTest { avatar = "avatar", ) ) - assertTrue(validated.isValid) + assertTrue(validated.isRight()) assertEquals( User.create( id = "id", @@ -31,8 +31,8 @@ class UserResponseToUserDomainMapperTest { firstName = "first", lastName = "last", avatar = "avatar", - ).valueOrThrow, - validated.valueOrThrow, + ).rightValueOrThrow, + validated.rightValueOrThrow, ) } @@ -47,10 +47,10 @@ class UserResponseToUserDomainMapperTest { avatar = "avatar", ) ) - assertTrue(validated.isInvalid) + assertTrue(validated.isLeft()) assertEquals( UserValidationError.INVALID_EMAIL_ADDRESS, - validated.invalidValueOrThrow.single(), + validated.leftValueOrThrow.single(), ) } } diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/Email.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/Email.kt index c0276bb9..3100750d 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/model/Email.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/Email.kt @@ -1,11 +1,11 @@ package com.hoc.flowmvi.domain.model -import com.hoc.flowmvi.core.ValidatedNes +import com.hoc.flowmvi.core.EitherNes @JvmInline value class Email private constructor(val value: String) { companion object { - fun create(value: String?): ValidatedNes = + fun create(value: String?): EitherNes = validateEmail(value).map(::Email) } } diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/FirstName.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/FirstName.kt index 0c54f5f8..13368849 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/model/FirstName.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/FirstName.kt @@ -1,11 +1,11 @@ package com.hoc.flowmvi.domain.model -import com.hoc.flowmvi.core.ValidatedNes +import com.hoc.flowmvi.core.EitherNes @JvmInline value class FirstName private constructor(val value: String) { companion object { - fun create(value: String?): ValidatedNes = + fun create(value: String?): EitherNes = validateFirstName(value).map(::FirstName) } } diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/LastName.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/LastName.kt index 59f62b20..b88224da 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/model/LastName.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/LastName.kt @@ -1,11 +1,11 @@ package com.hoc.flowmvi.domain.model -import com.hoc.flowmvi.core.ValidatedNes +import com.hoc.flowmvi.core.EitherNes @JvmInline value class LastName private constructor(val value: String) { companion object { - fun create(value: String?): ValidatedNes = + fun create(value: String?): EitherNes = validateLastName(value).map(::LastName) } } diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt index bca26ec3..7a067970 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/User.kt @@ -1,10 +1,9 @@ package com.hoc.flowmvi.domain.model -import arrow.core.zip -import arrow.typeclasses.Semigroup -import com.hoc.flowmvi.core.ValidatedNes -import com.hoc.flowmvi.core.nonEmptySet -import com.hoc.flowmvi.core.validNes +import arrow.core.Either +import com.hoc.flowmvi.core.EitherNes +import com.hoc.flowmvi.core.rightNes +import com.hoc.flowmvi.core.zipOrAccumulateNonEmptySet data class User( val id: String, @@ -20,45 +19,44 @@ data class User( firstName: String?, lastName: String?, avatar: String, - ): ValidatedNes = Email.create(email) - .zip( - Semigroup.nonEmptySet(), - FirstName.create(firstName), - LastName.create(lastName), - ) { e, f, l -> - User( - firstName = f, - email = e, - lastName = l, - id = id, - avatar = avatar - ) - } + ): EitherNes = Either.zipOrAccumulateNonEmptySet( + Email.create(email), + FirstName.create(firstName), + LastName.create(lastName) + ) { e, f, l -> + User( + firstName = f, + email = e, + lastName = l, + id = id, + avatar = avatar + ) + } } } -internal fun validateFirstName(firstName: String?): ValidatedNes { +internal fun validateFirstName(firstName: String?): EitherNes { if (firstName == null || firstName.length < MIN_LENGTH_FIRST_NAME) { - return UserValidationError.TOO_SHORT_FIRST_NAME.asInvalidNes + return UserValidationError.TOO_SHORT_FIRST_NAME.asLeftNes } // more validations here - return firstName.validNes() + return firstName.rightNes() } -internal fun validateLastName(lastName: String?): ValidatedNes { +internal fun validateLastName(lastName: String?): EitherNes { if (lastName == null || lastName.length < MIN_LENGTH_LAST_NAME) { - return UserValidationError.TOO_SHORT_LAST_NAME.asInvalidNes + return UserValidationError.TOO_SHORT_LAST_NAME.asLeftNes } // more validations here - return lastName.validNes() + return lastName.rightNes() } -internal fun validateEmail(email: String?): ValidatedNes { +internal fun validateEmail(email: String?): EitherNes { if (email == null || !EMAIL_ADDRESS_REGEX.matches(email)) { - return UserValidationError.INVALID_EMAIL_ADDRESS.asInvalidNes + return UserValidationError.INVALID_EMAIL_ADDRESS.asLeftNes } // more validations here - return email.validNes() + return email.rightNes() } private const val MIN_LENGTH_FIRST_NAME = 3 diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt index 33a723ec..26e1493d 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/UserError.kt @@ -1,10 +1,12 @@ package com.hoc.flowmvi.domain.model +import com.hoc.flowmvi.core.NonEmptySet + sealed class UserError : Throwable() { object NetworkError : UserError() data class UserNotFound(val id: String) : UserError() data class InvalidId(val id: String) : UserError() - data class ValidationFailed(val errors: Set) : UserError() + data class ValidationFailed(val errors: NonEmptySet) : UserError() object ServerError : UserError() object Unexpected : UserError() } diff --git a/domain/src/main/java/com/hoc/flowmvi/domain/model/UserValidationError.kt b/domain/src/main/java/com/hoc/flowmvi/domain/model/UserValidationError.kt index b71fd03b..1aedce2e 100644 --- a/domain/src/main/java/com/hoc/flowmvi/domain/model/UserValidationError.kt +++ b/domain/src/main/java/com/hoc/flowmvi/domain/model/UserValidationError.kt @@ -1,24 +1,26 @@ package com.hoc.flowmvi.domain.model +import com.hoc.flowmvi.core.EitherNes import com.hoc.flowmvi.core.NonEmptySet import com.hoc.flowmvi.core.NonEmptySet.Companion.toNonEmptySetOrNull -import com.hoc.flowmvi.core.ValidatedNes -import com.hoc.flowmvi.core.invalidNes +import com.hoc.flowmvi.core.leftNes enum class UserValidationError { INVALID_EMAIL_ADDRESS, TOO_SHORT_FIRST_NAME, TOO_SHORT_LAST_NAME; - val asInvalidNes: ValidatedNes = invalidNes() + val asLeftNes: EitherNes = leftNes() companion object { /** * Use this instead of [values()] for more performant. * See [KT-48872](https://youtrack.jetbrains.com/issue/KT-48872) */ + @JvmField val VALUES: List = values().asList() + @JvmField val VALUES_SET: NonEmptySet = VALUES.toNonEmptySetOrNull()!! } } diff --git a/domain/src/test/java/com/hoc/flowmvi/domain/Email_FirstName_LastName_Test.kt b/domain/src/test/java/com/hoc/flowmvi/domain/Email_FirstName_LastName_Test.kt index f0797439..24f12124 100644 --- a/domain/src/test/java/com/hoc/flowmvi/domain/Email_FirstName_LastName_Test.kt +++ b/domain/src/test/java/com/hoc/flowmvi/domain/Email_FirstName_LastName_Test.kt @@ -1,10 +1,10 @@ package com.hoc.flowmvi.ui.add -import arrow.core.orNull import com.hoc.flowmvi.domain.model.Email import com.hoc.flowmvi.domain.model.FirstName import com.hoc.flowmvi.domain.model.LastName import com.hoc.flowmvi.domain.model.UserValidationError +import com.hoc.flowmvi.test_utils.rightValueOrThrow import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -16,29 +16,29 @@ class Email_FirstName_LastName_Test { @Test fun testCreateEmail_withValidEmail_returnsValid() { val validated = Email.create("hoc081098@gmail.com") - assertTrue(validated.isValid) + assertTrue(validated.isRight()) assertEquals( "hoc081098@gmail.com", - validated.orNull()?.value, + validated.rightValueOrThrow.value, ) } @Test fun testCreateEmail_withInvalidEmail_returnsInvalid() { assertEquals( - UserValidationError.INVALID_EMAIL_ADDRESS.asInvalidNes, + UserValidationError.INVALID_EMAIL_ADDRESS.asLeftNes, Email.create(null), ) assertEquals( - UserValidationError.INVALID_EMAIL_ADDRESS.asInvalidNes, + UserValidationError.INVALID_EMAIL_ADDRESS.asLeftNes, Email.create(""), ) assertEquals( - UserValidationError.INVALID_EMAIL_ADDRESS.asInvalidNes, + UserValidationError.INVALID_EMAIL_ADDRESS.asLeftNes, Email.create("a"), ) assertEquals( - UserValidationError.INVALID_EMAIL_ADDRESS.asInvalidNes, + UserValidationError.INVALID_EMAIL_ADDRESS.asLeftNes, Email.create("a@"), ) } @@ -46,29 +46,29 @@ class Email_FirstName_LastName_Test { @Test fun testCreateFirstName_withValidFirstName_returnsValid() { val validated = FirstName.create("hoc081098") - assertTrue(validated.isValid) + assertTrue(validated.isRight()) assertEquals( "hoc081098", - validated.orNull()?.value, + validated.rightValueOrThrow.value, ) } @Test fun testCreateFirstName_withInvalidFirstName_returnsInvalid() { assertEquals( - UserValidationError.TOO_SHORT_FIRST_NAME.asInvalidNes, + UserValidationError.TOO_SHORT_FIRST_NAME.asLeftNes, FirstName.create(null), ) assertEquals( - UserValidationError.TOO_SHORT_FIRST_NAME.asInvalidNes, + UserValidationError.TOO_SHORT_FIRST_NAME.asLeftNes, FirstName.create(""), ) assertEquals( - UserValidationError.TOO_SHORT_FIRST_NAME.asInvalidNes, + UserValidationError.TOO_SHORT_FIRST_NAME.asLeftNes, FirstName.create("a"), ) assertEquals( - UserValidationError.TOO_SHORT_FIRST_NAME.asInvalidNes, + UserValidationError.TOO_SHORT_FIRST_NAME.asLeftNes, FirstName.create("ab"), ) } @@ -76,29 +76,29 @@ class Email_FirstName_LastName_Test { @Test fun testCreateLastName_withValidLastName_returnsValid() { val validated = LastName.create("hoc081098") - assertTrue(validated.isValid) + assertTrue(validated.isRight()) assertEquals( "hoc081098", - validated.orNull()?.value, + validated.rightValueOrThrow.value, ) } @Test fun testCreateLastName_withInvalidLastName_returnsInvalid() { assertEquals( - UserValidationError.TOO_SHORT_LAST_NAME.asInvalidNes, + UserValidationError.TOO_SHORT_LAST_NAME.asLeftNes, LastName.create(null), ) assertEquals( - UserValidationError.TOO_SHORT_LAST_NAME.asInvalidNes, + UserValidationError.TOO_SHORT_LAST_NAME.asLeftNes, LastName.create(""), ) assertEquals( - UserValidationError.TOO_SHORT_LAST_NAME.asInvalidNes, + UserValidationError.TOO_SHORT_LAST_NAME.asLeftNes, LastName.create("a"), ) assertEquals( - UserValidationError.TOO_SHORT_LAST_NAME.asInvalidNes, + UserValidationError.TOO_SHORT_LAST_NAME.asLeftNes, LastName.create("ab"), ) } diff --git a/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt b/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt index a0bbb45e..7b75e88d 100644 --- a/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt +++ b/domain/src/test/java/com/hoc/flowmvi/domain/UseCaseTest.kt @@ -11,7 +11,7 @@ import com.hoc.flowmvi.domain.usecase.RefreshGetUsersUseCase import com.hoc.flowmvi.domain.usecase.RemoveUserUseCase import com.hoc.flowmvi.domain.usecase.SearchUsersUseCase import com.hoc.flowmvi.test_utils.TestCoroutineDispatcherRule -import com.hoc.flowmvi.test_utils.valueOrThrow +import com.hoc.flowmvi.test_utils.rightValueOrThrow import io.mockk.clearAllMocks import io.mockk.coEvery import io.mockk.coVerify @@ -51,7 +51,7 @@ private val USERS = listOf( lastName = "last3", avatar = "3.png" ), -).map { it.valueOrThrow } +).map { it.rightValueOrThrow } @ExperimentalCoroutinesApi class UseCaseTest { diff --git a/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt b/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt index 79d952e9..348ac36f 100644 --- a/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt +++ b/domain/src/test/java/com/hoc/flowmvi/domain/UserTest.kt @@ -2,8 +2,8 @@ package com.hoc.flowmvi.domain import com.hoc.flowmvi.domain.model.User import com.hoc.flowmvi.domain.model.UserValidationError -import com.hoc.flowmvi.test_utils.invalidValueOrThrow -import com.hoc.flowmvi.test_utils.valueOrThrow +import com.hoc.flowmvi.test_utils.leftValueOrThrow +import com.hoc.flowmvi.test_utils.rightValueOrThrow import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertTrue @@ -23,8 +23,8 @@ class UserTest { lastName = VALID_NAME, avatar = AVATAR, ) - assertTrue(validated.isValid) - validated.valueOrThrow.let { user -> + assertTrue(validated.isRight()) + validated.rightValueOrThrow.let { user -> assertEquals(ID, user.id) assertEquals(VALID_EMAIL, user.email.value) assertEquals(VALID_NAME, user.firstName.value) @@ -42,8 +42,8 @@ class UserTest { lastName = VALID_NAME, avatar = AVATAR, ) - assertTrue(validated.isInvalid) - assertEquals(UserValidationError.INVALID_EMAIL_ADDRESS, validated.invalidValueOrThrow.single()) + assertTrue(validated.isLeft()) + assertEquals(UserValidationError.INVALID_EMAIL_ADDRESS, validated.leftValueOrThrow.single()) } @Test @@ -55,8 +55,8 @@ class UserTest { lastName = VALID_NAME, avatar = AVATAR, ) - assertTrue(validated.isInvalid) - assertEquals(UserValidationError.TOO_SHORT_FIRST_NAME, validated.invalidValueOrThrow.single()) + assertTrue(validated.isLeft()) + assertEquals(UserValidationError.TOO_SHORT_FIRST_NAME, validated.leftValueOrThrow.single()) } @Test @@ -68,8 +68,8 @@ class UserTest { lastName = "h", avatar = AVATAR, ) - assertTrue(validated.isInvalid) - assertEquals(UserValidationError.TOO_SHORT_LAST_NAME, validated.invalidValueOrThrow.single()) + assertTrue(validated.isLeft()) + assertEquals(UserValidationError.TOO_SHORT_LAST_NAME, validated.leftValueOrThrow.single()) } @Test @@ -81,13 +81,13 @@ class UserTest { lastName = VALID_NAME, avatar = AVATAR, ) - assertTrue(validated.isInvalid) + assertTrue(validated.isLeft()) assertEquals( setOf( UserValidationError.INVALID_EMAIL_ADDRESS, UserValidationError.TOO_SHORT_FIRST_NAME, ), - validated.invalidValueOrThrow.toSet() + validated.leftValueOrThrow.toSet() ) } @@ -100,13 +100,13 @@ class UserTest { lastName = "h", avatar = AVATAR, ) - assertTrue(validated.isInvalid) + assertTrue(validated.isLeft()) assertEquals( setOf( UserValidationError.INVALID_EMAIL_ADDRESS, UserValidationError.TOO_SHORT_LAST_NAME, ), - validated.invalidValueOrThrow.toSet() + validated.leftValueOrThrow.toSet() ) } @@ -119,13 +119,13 @@ class UserTest { lastName = "h", avatar = AVATAR, ) - assertTrue(validated.isInvalid) + assertTrue(validated.isLeft()) assertEquals( setOf( UserValidationError.TOO_SHORT_FIRST_NAME, UserValidationError.TOO_SHORT_LAST_NAME, ), - validated.invalidValueOrThrow.toSet() + validated.leftValueOrThrow.toSet() ) } @@ -138,10 +138,10 @@ class UserTest { lastName = "h", avatar = AVATAR, ) - assertTrue(validated.isInvalid) + assertTrue(validated.isLeft()) assertEquals( UserValidationError.values().toSet(), - validated.invalidValueOrThrow.toSet() + validated.leftValueOrThrow.toSet() ) } } diff --git a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt index 0b56a350..49ec7c0a 100644 --- a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt +++ b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddContract.kt @@ -4,7 +4,7 @@ import android.os.Bundle import android.os.Parcelable import androidx.core.os.bundleOf import arrow.core.identity -import com.hoc.flowmvi.core.ValidatedNes +import com.hoc.flowmvi.core.EitherNes import com.hoc.flowmvi.domain.model.User import com.hoc.flowmvi.domain.model.UserError import com.hoc.flowmvi.domain.model.UserValidationError @@ -71,15 +71,15 @@ internal sealed interface PartialStateChange { val email: String, val firstName: String, val lastName: String, - val userValidatedNes: ValidatedNes, + val userEitherNes: EitherNes, ) : PartialStateChange { override fun reduce(viewState: ViewState): ViewState = viewState.copy( email = email, firstName = firstName, lastName = lastName, - errors = userValidatedNes.fold( - fe = ::identity, - fa = { emptySet() }, + errors = userEitherNes.fold( + ifLeft = ::identity, + ifRight = { emptySet() }, ), ) } diff --git a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt index 2f9d1e42..21d0eb24 100644 --- a/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt +++ b/feature-add/src/main/java/com/hoc/flowmvi/ui/add/AddVM.kt @@ -2,7 +2,6 @@ package com.hoc.flowmvi.ui.add import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.viewModelScope -import arrow.core.orNull import com.hoc.flowmvi.domain.model.User import com.hoc.flowmvi.domain.usecase.AddUserUseCase import com.hoc.flowmvi.mvi_base.AbstractMviViewModel @@ -82,7 +81,7 @@ class AddVM( email = email, firstName = firstName, lastName = lastName, - userValidatedNes = User.create( + userEitherNes = User.create( email = email, firstName = firstName, lastName = lastName, @@ -118,9 +117,9 @@ class AddVM( ) private fun Flow.toAddUserChangeFlow(userFormFlow: SharedFlow): Flow = - withLatestFrom(userFormFlow) { _, userForm -> userForm.userValidatedNes } + withLatestFrom(userFormFlow) { _, userForm -> userForm.userEitherNes } .debugLog("toAddUserChangeFlow::userValidatedNel") - .mapNotNull { it.orNull() } + .mapNotNull { it.getOrNull() } .flatMapFirst { user -> flowFromSuspend { addUser(user) } .map { result -> diff --git a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt index b7b5a7a1..9e8bf1b0 100644 --- a/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt +++ b/feature-add/src/test/java/com/hoc/flowmvi/ui/add/AddVMTest.kt @@ -12,7 +12,7 @@ import com.hoc.flowmvi.domain.usecase.AddUserUseCase import com.hoc.flowmvi.mvi_testing.BaseMviViewModelTest import com.hoc.flowmvi.mvi_testing.mapRight import com.hoc.flowmvi.mvi_testing.returnsWithDelay -import com.hoc.flowmvi.test_utils.valueOrThrow +import com.hoc.flowmvi.test_utils.rightValueOrThrow import io.mockk.coEvery import io.mockk.coVerify import io.mockk.confirmVerified @@ -165,7 +165,7 @@ class AddVMTest : BaseMviViewModelTest(object : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: UserItem, newItem: UserItem) = oldItem.id == newItem.id + + @SuppressLint("DiffUtilEquals") // `==` is generated by Poko plugin. override fun areContentsTheSame(oldItem: UserItem, newItem: UserItem) = oldItem == newItem }) { diff --git a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchContract.kt b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchContract.kt index f4399a4c..546dd88f 100644 --- a/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchContract.kt +++ b/feature-search/src/main/java/com/hoc/flowmvi/ui/search/SearchContract.kt @@ -8,11 +8,9 @@ import com.hoc.flowmvi.mvi_base.MviIntent import com.hoc.flowmvi.mvi_base.MviSingleEvent import com.hoc.flowmvi.mvi_base.MviViewState import com.hoc.flowmvi.mvi_base.MviViewStateSaver -import dev.ahmedmourad.nocopy.annotations.NoCopy +import dev.drewhamilton.poko.Poko -@Suppress("DataClassPrivateConstructor") -@NoCopy -data class UserItem private constructor( +@Poko class UserItem private constructor( val id: String, val email: String, val avatar: String, diff --git a/feature-search/src/test/java/com/hoc/flowmvi/ui/search/SearchContractTest.kt b/feature-search/src/test/java/com/hoc/flowmvi/ui/search/SearchContractTest.kt index 907091c9..aa5310ba 100644 --- a/feature-search/src/test/java/com/hoc/flowmvi/ui/search/SearchContractTest.kt +++ b/feature-search/src/test/java/com/hoc/flowmvi/ui/search/SearchContractTest.kt @@ -1,7 +1,7 @@ package com.hoc.flowmvi.ui.search import com.hoc.flowmvi.domain.model.User -import com.hoc.flowmvi.test_utils.valueOrThrow +import com.hoc.flowmvi.test_utils.rightValueOrThrow import kotlin.test.Test import kotlin.test.assertEquals @@ -44,6 +44,6 @@ class SearchContractTest { avatar = AVATAR, firstName = FIRST_NAME, lastName = LAST_NAME - ).valueOrThrow + ).rightValueOrThrow } } diff --git a/feature-search/src/test/java/com/hoc/flowmvi/ui/search/TestData.kt b/feature-search/src/test/java/com/hoc/flowmvi/ui/search/TestData.kt index 256ea616..42268882 100644 --- a/feature-search/src/test/java/com/hoc/flowmvi/ui/search/TestData.kt +++ b/feature-search/src/test/java/com/hoc/flowmvi/ui/search/TestData.kt @@ -1,7 +1,7 @@ package com.hoc.flowmvi.ui.search import com.hoc.flowmvi.domain.model.User -import com.hoc.flowmvi.test_utils.valueOrThrow +import com.hoc.flowmvi.test_utils.rightValueOrThrow internal val USERS = listOf( User.create( @@ -25,6 +25,6 @@ internal val USERS = listOf( lastName = "last3", avatar = "3.png" ), -).map { it.valueOrThrow } +).map { it.rightValueOrThrow } internal val USER_ITEMS = USERS.map(UserItem::from) diff --git a/test-utils/src/main/java/com/hoc/flowmvi/test_utils/utils.kt b/test-utils/src/main/java/com/hoc/flowmvi/test_utils/utils.kt index 1f09c584..9d1465b7 100644 --- a/test-utils/src/main/java/com/hoc/flowmvi/test_utils/utils.kt +++ b/test-utils/src/main/java/com/hoc/flowmvi/test_utils/utils.kt @@ -1,24 +1,11 @@ package com.hoc.flowmvi.test_utils import arrow.core.Either -import arrow.core.Validated -import arrow.core.getOrHandle +import arrow.core.getOrElse import arrow.core.identity -import arrow.core.valueOr -inline val Validated.valueOrThrow: A - get() = valueOr(this::throws) +inline val Either.leftValueOrThrow: L + get() = fold(::identity) { throw AssertionError("Expect a Left but got a $this") } -inline val Validated.invalidValueOrThrow: E - get() = fold(::identity, this::throws) - -inline val Either.leftOrThrow: L - get() = fold(::identity, this::throws) - -inline val Either.getOrThrow: R - get() = getOrHandle(this::throws) - -@PublishedApi -internal fun Any.throws(it: E): Nothing = - if (it is Throwable) throw it - else error("$this - $it - Should not reach here!") +inline val Either.rightValueOrThrow: R + get() = getOrElse { throw AssertionError("Expect a Right but got a $this") }