diff --git a/build.gradle.kts b/build.gradle.kts index 24ccde8..9a815fb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -89,10 +89,10 @@ subprojects { repo = "maven" name = "base-types-kt" desc = "The modelling for success/failure of operations in Kotlin" - userOrg = "kittinunf" - websiteUrl = "https://github.com/krzykrucz/base-types-kt" - vcsUrl = "https://github.com/krzykrucz/base-types-kt" - setLicenses("MIT") + userOrg = "VirtusLab" + websiteUrl = "https://github.com/VirtusLab/base-types-kt" + vcsUrl = "https://github.com/VirtusLab/base-types-kt" + setLicenses("Apache License 2.0") version.apply { name = artifactPublish } diff --git a/result-arrow/build.gradle.kts b/result-arrow/build.gradle.kts index 04d615b..1faaa6f 100644 --- a/result-arrow/build.gradle.kts +++ b/result-arrow/build.gradle.kts @@ -17,7 +17,7 @@ dependencies { implementation("io.arrow-kt:arrow-core:$arrow") implementation("io.arrow-kt:arrow-fx:$arrow") - implementation("com.github.kittinunf.result:result:2.2.0") + implementation(project(":result")) testImplementation("io.kotlintest:kotlintest-runner-junit5:$kotlintest") diff --git a/result-arrow/src/main/kotlin/com/virtuslab/basetypes/result/arrow/AsyncResult.kt b/result-arrow/src/main/kotlin/com/virtuslab/basetypes/result/arrow/AsyncResult.kt index 1300718..f6cbc57 100644 --- a/result-arrow/src/main/kotlin/com/virtuslab/basetypes/result/arrow/AsyncResult.kt +++ b/result-arrow/src/main/kotlin/com/virtuslab/basetypes/result/arrow/AsyncResult.kt @@ -1,10 +1,10 @@ package com.virtuslab.basetypes.result.arrow import arrow.fx.IO -import com.github.kittinunf.result.Result -import com.github.kittinunf.result.Result.Failure -import com.github.kittinunf.result.Result.Success -import com.github.kittinunf.result.flatMap +import com.virtuslab.basetypes.result.Result +import com.virtuslab.basetypes.result.Result.Failure +import com.virtuslab.basetypes.result.Result.Success +import com.virtuslab.basetypes.result.flatMap typealias AsyncResult = IO> diff --git a/result-arrow/src/test/kotlin/com/virtuslab/basetypes/result/arrow/AsyncResultKtTest.kt b/result-arrow/src/test/kotlin/com/virtuslab/basetypes/result/arrow/AsyncResultKtTest.kt index eaaa0cf..ee23870 100644 --- a/result-arrow/src/test/kotlin/com/virtuslab/basetypes/result/arrow/AsyncResultKtTest.kt +++ b/result-arrow/src/test/kotlin/com/virtuslab/basetypes/result/arrow/AsyncResultKtTest.kt @@ -1,6 +1,6 @@ package com.virtuslab.basetypes.result.arrow -import com.github.kittinunf.result.Result +import com.virtuslab.basetypes.result.Result import io.kotlintest.shouldBe import io.kotlintest.shouldThrow import org.junit.jupiter.api.Test diff --git a/result-reactor/build.gradle.kts b/result-reactor/build.gradle.kts index e9f2b04..64525e9 100644 --- a/result-reactor/build.gradle.kts +++ b/result-reactor/build.gradle.kts @@ -16,7 +16,7 @@ dependencies { implementation(kotlin("stdlib", kotlinVersion)) implementation("io.projectreactor:reactor-core:$reactor") - implementation("com.github.kittinunf.result:result:2.2.0") + implementation(project(":result")) testImplementation("io.kotlintest:kotlintest-runner-junit5:$kotlintest") diff --git a/result-reactor/src/main/kotlin/com/virtuslab/basetypes/result/reactor/MonoResult.kt b/result-reactor/src/main/kotlin/com/virtuslab/basetypes/result/reactor/MonoResult.kt index f9242fd..325c85e 100644 --- a/result-reactor/src/main/kotlin/com/virtuslab/basetypes/result/reactor/MonoResult.kt +++ b/result-reactor/src/main/kotlin/com/virtuslab/basetypes/result/reactor/MonoResult.kt @@ -1,11 +1,11 @@ package com.virtuslab.basetypes.result.reactor -import com.github.kittinunf.result.Result -import com.github.kittinunf.result.Result.Failure -import com.github.kittinunf.result.Result.Success -import com.github.kittinunf.result.flatMap -import com.github.kittinunf.result.map -import com.github.kittinunf.result.mapError +import com.virtuslab.basetypes.result.Result +import com.virtuslab.basetypes.result.Result.Failure +import com.virtuslab.basetypes.result.Result.Success +import com.virtuslab.basetypes.result.flatMap +import com.virtuslab.basetypes.result.map +import com.virtuslab.basetypes.result.mapError import reactor.core.publisher.Mono import reactor.core.publisher.toMono @@ -31,11 +31,7 @@ fun MonoResult.flatMapResult(mapper: (S fun MonoResult.flatMapSuccess(mapper: (S) -> MonoResult): MonoResult = this.flatMap { result1 -> when (result1) { - is Success -> try { - mapper(result1.value) - } catch (ex: Exception) { - Failure(ex as E).toMono() - } + is Success -> mapper(result1.value) is Failure -> Failure(result1.error).toMono() } } diff --git a/result-reactor/src/test/kotlin/com/virtuslab/basetypes/result/reactor/MonoResultKtTest.kt b/result-reactor/src/test/kotlin/com/virtuslab/basetypes/result/reactor/MonoResultKtTest.kt index e4e0c2a..616db23 100644 --- a/result-reactor/src/test/kotlin/com/virtuslab/basetypes/result/reactor/MonoResultKtTest.kt +++ b/result-reactor/src/test/kotlin/com/virtuslab/basetypes/result/reactor/MonoResultKtTest.kt @@ -1,6 +1,6 @@ package com.virtuslab.basetypes.result.reactor -import com.github.kittinunf.result.Result +import com.virtuslab.basetypes.result.Result import io.kotlintest.specs.StringSpec import reactor.core.publisher.Mono import reactor.core.publisher.toMono @@ -24,8 +24,7 @@ internal class MonoResultKtTest : StringSpec() { monoResult.mapSuccess { if (true) throw runtimeException else "Some other value" } .test() - .expectNext(Result.error(runtimeException) as Result) - .verifyComplete() + .verifyError(RuntimeException::class.java) } "should handle exception when mapping result success" { @@ -34,8 +33,7 @@ internal class MonoResultKtTest : StringSpec() { monoResult.flatMapResult { if (true) throw runtimeException else Result.success("Some other value") } .test() - .expectNext(Result.error(runtimeException) as Result) - .verifyComplete() + .verifyError(RuntimeException::class.java) } "should handle exception when flatMapping MonoResult" { @@ -44,8 +42,7 @@ internal class MonoResultKtTest : StringSpec() { monoResult.flatMapSuccess { if (true) throw runtimeException else "Some other value".justMonoResult() } .test() - .expectNext(Result.error(runtimeException) as Result) - .verifyComplete() + .verifyError(RuntimeException::class.java) } "should keep error when mapping success" { diff --git a/result-rxjava/build.gradle.kts b/result-rxjava/build.gradle.kts index 6ecf78e..368edda 100644 --- a/result-rxjava/build.gradle.kts +++ b/result-rxjava/build.gradle.kts @@ -16,7 +16,7 @@ dependencies { implementation(kotlin("stdlib", kotlinVersion)) implementation("io.reactivex.rxjava2:rxjava:2.2.14") - implementation("com.github.kittinunf.result:result:2.2.0") + implementation(project(":result")) implementation("io.arrow-kt:arrow-core:$arrow") testImplementation("io.kotlintest:kotlintest-runner-junit5:$kotlintest") diff --git a/result-rxjava/src/main/kotlin/com/virtuslab/basetypes/result/rxjava/SingleResult.kt b/result-rxjava/src/main/kotlin/com/virtuslab/basetypes/result/rxjava/SingleResult.kt index f153402..e8e4b59 100644 --- a/result-rxjava/src/main/kotlin/com/virtuslab/basetypes/result/rxjava/SingleResult.kt +++ b/result-rxjava/src/main/kotlin/com/virtuslab/basetypes/result/rxjava/SingleResult.kt @@ -3,12 +3,12 @@ package com.virtuslab.basetypes.result.rxjava import arrow.core.None import arrow.core.Option import arrow.core.Some -import com.github.kittinunf.result.Result -import com.github.kittinunf.result.Result.Failure -import com.github.kittinunf.result.Result.Success -import com.github.kittinunf.result.flatMap -import com.github.kittinunf.result.map -import com.github.kittinunf.result.mapError +import com.virtuslab.basetypes.result.Result +import com.virtuslab.basetypes.result.Result.Failure +import com.virtuslab.basetypes.result.Result.Success +import com.virtuslab.basetypes.result.flatMap +import com.virtuslab.basetypes.result.map +import com.virtuslab.basetypes.result.mapError import io.reactivex.Single typealias SingleResult = Single> @@ -32,11 +32,7 @@ fun SingleResult.flatMapResult(mapper: fun SingleResult.flatMapSuccess(mapper: (S) -> SingleResult): SingleResult = this.flatMap { result1 -> when (result1) { - is Success -> try { - mapper(result1.value) - } catch (ex: Exception) { - Failure(ex as E).toSingle() - } + is Success -> mapper(result1.value) is Failure -> Failure(result1.error).toSingle() } } @@ -76,8 +72,6 @@ fun Single.liftResult(errorMapper: (Throwable) -> E) this.map { Result.success(it) as Result } .onErrorReturn { Result.error(errorMapper(it)) } -//fun SingleResult.any(predicate: (V) -> Boolean): Boolean = TODO() - fun S.toSingle(): Single = Single.just(this) // TODO test diff --git a/result-rxjava/src/test/kotlin/com/virtuslab/basetypes/result/rxjava/SingleResultKtTest.kt b/result-rxjava/src/test/kotlin/com/virtuslab/basetypes/result/rxjava/SingleResultKtTest.kt index d5513cb..8635d5c 100644 --- a/result-rxjava/src/test/kotlin/com/virtuslab/basetypes/result/rxjava/SingleResultKtTest.kt +++ b/result-rxjava/src/test/kotlin/com/virtuslab/basetypes/result/rxjava/SingleResultKtTest.kt @@ -1,6 +1,6 @@ package com.virtuslab.basetypes.result.rxjava -import com.github.kittinunf.result.Result +import com.virtuslab.basetypes.result.Result import io.kotlintest.specs.StringSpec import io.reactivex.Single @@ -16,34 +16,31 @@ internal class SingleResultKtTest: StringSpec() { .assertComplete() } - "should handle exception when mapping success" { + "should propagate exception when mapping success" { val monoResult: SingleResult = "Some value".justSingleResult() val runtimeException = RuntimeException() monoResult.mapSuccess { if (true) throw runtimeException else "Some other value" } .test() - .assertResult(Result.error(runtimeException) as Result) - .assertComplete() + .assertError(runtimeException) } - "should handle exception when mapping result success" { + "should propagate exception when mapping result success" { val monoResult: SingleResult = "Some value".justSingleResult() val runtimeException = RuntimeException() monoResult.flatMapResult { if (true) throw runtimeException else Result.success("Some other value") } .test() - .assertResult(Result.error(runtimeException) as Result) - .assertComplete() + .assertError(runtimeException) } - "should handle exception when flatMapping SingleResult" { + "should propagate exception when flatMapping SingleResult" { val monoResult: SingleResult = "Some value".justSingleResult() val runtimeException = RuntimeException() monoResult.flatMapSuccess { if (true) throw runtimeException else "Some other value".justSingleResult() } .test() - .assertResult(Result.error(runtimeException) as Result) - .assertComplete() + .assertError(runtimeException) } "should keep error when mapping success" { diff --git a/result/.gitignore b/result/.gitignore new file mode 100644 index 0000000..a774788 --- /dev/null +++ b/result/.gitignore @@ -0,0 +1,2 @@ +/build + diff --git a/result/LICENSE.md b/result/LICENSE.md new file mode 100644 index 0000000..22aae75 --- /dev/null +++ b/result/LICENSE.md @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Kittinun Vantasin + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/result/build.gradle.kts b/result/build.gradle.kts new file mode 100644 index 0000000..1971518 --- /dev/null +++ b/result/build.gradle.kts @@ -0,0 +1,15 @@ +sourceSets { + getByName("main").java.srcDirs("src/main/kotlin") + getByName("test").java.srcDirs("src/main/kotlin") +} + +dependencies { + val kotlinVersion: String by project + val junit: String by project + + implementation(kotlin("stdlib", kotlinVersion)) + + testImplementation("junit:junit:4.12") + + +} diff --git a/result/src/main/kotlin/com/virtuslab/basetypes/result/NoException.kt b/result/src/main/kotlin/com/virtuslab/basetypes/result/NoException.kt new file mode 100644 index 0000000..b659186 --- /dev/null +++ b/result/src/main/kotlin/com/virtuslab/basetypes/result/NoException.kt @@ -0,0 +1,3 @@ +package com.virtuslab.basetypes.result + +class NoException private constructor() : Exception() \ No newline at end of file diff --git a/result/src/main/kotlin/com/virtuslab/basetypes/result/Result.kt b/result/src/main/kotlin/com/virtuslab/basetypes/result/Result.kt new file mode 100644 index 0000000..18c6e3e --- /dev/null +++ b/result/src/main/kotlin/com/virtuslab/basetypes/result/Result.kt @@ -0,0 +1,133 @@ +package com.virtuslab.basetypes.result + +inline fun Result<*, *>.getAs() = when (this) { + is Result.Success -> value as? X + is Result.Failure -> error as? X +} + +fun Result.success(f: (V) -> Unit) = fold(f, {}) + +fun Result<*, E>.failure(f: (E) -> Unit) = fold({}, f) + +infix fun Result.or(fallback: V) = when (this) { + is Result.Success -> this + else -> Result.Success(fallback) +} + +infix fun Result.getOrElse(fallback: V) = when (this) { + is Result.Success -> value + else -> fallback +} + +inline fun Result.map(transform: (V) -> U): Result = + when (this) { + is Result.Success -> Result.Success(transform(value)) + is Result.Failure -> Result.Failure(error) + } + +inline fun Result.flatMap(transform: (V) -> Result): Result = + when (this) { + is Result.Success -> transform(value) + is Result.Failure -> Result.Failure(error) + } + +fun Result.mapError(transform: (E) -> E2) = when (this) { + is Result.Success -> Result.Success(value) + is Result.Failure -> Result.Failure(transform(error)) +} + +fun Result.flatMapError(transform: (E) -> Result) = when (this) { + is Result.Success -> Result.Success(value) + is Result.Failure -> transform(error) +} + +fun Result.any(predicate: (V) -> Boolean): Boolean = try { + when (this) { + is Result.Success -> predicate(value) + is Result.Failure -> false + } +} catch (ex: Exception) { + false +} + +fun Result.zip(other: () -> Result): Result, *> = + flatMap { outer -> other().map { outer to it } } + +fun List>.sequence(): Result, E> = fold(Result.success(mutableListOf()) as Result, E>) { acc, result -> + acc.flatMap { combine -> + result.map { combine.apply { add(it) } } + } +} + +fun T.toResult() = Result.success(this) + +fun E.toResultFailure() = Result.error(this) + +sealed class Result { + + open operator fun component1(): V? = null + open operator fun component2(): E? = null + + inline fun fold(success: (V) -> X, failure: (E) -> X): X = when (this) { + is Success -> success(this.value) + is Failure -> failure(this.error) + } + + abstract fun get(): V + + abstract fun isSuccess(): Boolean + abstract fun isFailure(): Boolean + + class Success(val value: V) : Result() { + override fun component1(): V? = value + + override fun get(): V = value + + override fun toString() = "[Success: $value]" + + override fun hashCode(): Int = value.hashCode() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + return other is Success<*> && value == other.value + } + + override fun isSuccess() = true + + override fun isFailure() = false + } + + class Failure(val error: E) : Result() { + override fun component2(): E? = error + + override fun get() = throw error + + fun getException(): E = error + + override fun toString() = "[Failure: $error]" + + override fun hashCode(): Int = error.hashCode() + + override fun equals(other: Any?): Boolean { + if (this === other) return true + return other is Failure<*> && error == other.error + } + + override fun isSuccess() = false + + override fun isFailure() = true + } + + companion object { + // Factory methods + fun error(ex: E) = Failure(ex) + + fun success(v: V) = Success(v) + + fun of(value: V?, fail: (() -> Exception) = { Exception() }): Result = + value?.let { success(it) } ?: error(fail()) + + fun of(f: () -> V): Result = success(f()) + } + +} diff --git a/result/src/main/kotlin/com/virtuslab/basetypes/result/Validation.kt b/result/src/main/kotlin/com/virtuslab/basetypes/result/Validation.kt new file mode 100644 index 0000000..8a8f1de --- /dev/null +++ b/result/src/main/kotlin/com/virtuslab/basetypes/result/Validation.kt @@ -0,0 +1,8 @@ +package com.virtuslab.basetypes.result + +class Validation(vararg resultSequence: Result<*, E>) { + + val failures: List = resultSequence.filterIsInstance>().map { it.getException() } + + val hasFailure = failures.isNotEmpty() +} diff --git a/result/src/test/assets/bar.txt b/result/src/test/assets/bar.txt new file mode 100644 index 0000000..ae75f35 --- /dev/null +++ b/result/src/test/assets/bar.txt @@ -0,0 +1 @@ +Contrary to popular belief, Lorem Ipsum is not simply random text. It has roots in a piece of classical Latin literature from 45 BC, making it over 2000 years old. Richard McClintock, a Latin professor at Hampden-Sydney College in Virginia, looked up one of the more obscure Latin words, consectetur, from a Lorem Ipsum passage, and going through the cites of the word in classical literature, discovered the undoubtable source. \ No newline at end of file diff --git a/result/src/test/assets/foo.txt b/result/src/test/assets/foo.txt new file mode 100644 index 0000000..e83bcc9 --- /dev/null +++ b/result/src/test/assets/foo.txt @@ -0,0 +1 @@ +Lorem Ipsum is simply dummy text of the printing and typesetting industry. Lorem Ipsum has been the industry's standard dummy text ever since the 1500s, when an unknown printer took a galley of type and scrambled it to make a type specimen book. It has survived not only five centuries, but also the leap into electronic typesetting, remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset sheets containing Lorem Ipsum passages, and more recently with desktop publishing software like Aldus PageMaker including versions of Lorem Ipsum. \ No newline at end of file diff --git a/result/src/test/kotlin/com/virtuslab/basetypes/result/ResultTests.kt b/result/src/test/kotlin/com/virtuslab/basetypes/result/ResultTests.kt new file mode 100644 index 0000000..3d9633f --- /dev/null +++ b/result/src/test/kotlin/com/virtuslab/basetypes/result/ResultTests.kt @@ -0,0 +1,422 @@ +package com.virtuslab.basetypes.result + +import org.hamcrest.CoreMatchers.equalTo +import org.hamcrest.CoreMatchers.instanceOf +import org.hamcrest.CoreMatchers.notNullValue +import org.hamcrest.CoreMatchers.nullValue +import org.junit.Assert.assertThat +import org.junit.Ignore +import org.junit.Test +import java.io.File +import java.io.FileNotFoundException + +class ResultTests { + + @Test + fun createValue() { + val v = Result.of(1) + + assertThat("Result is created successfully", v, notNullValue()) + assertThat("v is Result.Success type", v, instanceOf(Result.Success::class.java)) + } + + @Test + fun createError() { + val e = Result.error(RuntimeException()) + + assertThat("Result is created successfully", e, notNullValue()) + assertThat("e is Result.Failure type", e, instanceOf(Result.Failure::class.java)) + } + + @Test + fun createOptionalValue() { + val value1: String? = null + val value2: String? = "1" + + val result1 = Result.of(value1) { UnsupportedOperationException("value is null") } + val result2 = Result.of(value2) { IllegalStateException("value is null") } + + assertThat("result1 is Result.Failure type", result1, instanceOf(Result.Failure::class.java)) + assertThat("result2 is Result.Success type", result2, instanceOf(Result.Success::class.java)) + } + + @Test + @Ignore + fun createFromLambda() { + val f1 = { "foo" } + val f2 = { + val v = arrayListOf() + v[1] + } + + val f3 = { + val s: String? + s = null + s + } + + val result1 = Result.of(f1) + val result2 = Result.of(f2) + val result3 = Result.of(f3()) + + assertThat("result1 is Result.Success type", result1, instanceOf(Result.Success::class.java)) + assertThat("result2 is Result.Failure type", result2, instanceOf(Result.Failure::class.java)) + assertThat("result3 is Result.Failure type", result3, instanceOf(Result.Failure::class.java)) + } + + @Test + fun or() { + val one = Result.of(null) or 1 + + assertThat("one is Result.Success type", one, instanceOf(Result.Success::class.java)) + assertThat("value one is 1", one.component1()!!, equalTo(1)) + } + + @Test + fun orElse() { + val one = Result.of(null) getOrElse 1 + + assertThat("one is 1", one, equalTo(1)) + } + + @Test + fun success() { + val result = Result.of { true } + + var beingCalled = false + result.success { + beingCalled = true + } + + var notBeingCalled = true + result.failure { + notBeingCalled = false + } + + assertThat(beingCalled, equalTo(true)) + assertThat(notBeingCalled, equalTo(true)) + } + + @Test(expected = FileNotFoundException::class) + fun failure() { + val result = Result.of { File("not_found_file").readText() } + + var beingCalled = false + result.failure { + beingCalled = true + } + + var notBeingCalled = true + result.success { + notBeingCalled = false + } + + assertThat(beingCalled, equalTo(true)) + assertThat(notBeingCalled, equalTo(true)) + } + + @Test + @Ignore + fun get() { + val f1 = { true } + val f2 = { File("not_found_file").readText() } + + val result1 = Result.of(f1) + val result2 = Result.of(f2) + + assertThat("result1 is true", result1.get(), equalTo(true)) + + var result = false + try { + result2.get() + } catch (e: FileNotFoundException) { + result = true + } + + assertThat("result2 expecting to throw FileNotFoundException", result, equalTo(true)) + } + + @Suppress("UNUSED_VARIABLE") + @Test + fun getAsValue() { + val result1 = Result.of(22) + val result2 = Result.error(KotlinNullPointerException()) + + val v1: Int = result1.getAs()!! + val (v2, err) = result2 + + assertThat("v1 is equal 22", v1, equalTo(22)) + assertThat("err is KotlinNullPointerException type", err is KotlinNullPointerException, equalTo(true)) + } + + @Test + fun fold() { + val success = Result.of("success") + val failure = Result.error(RuntimeException("failure")) + + val v1 = success.fold({ 1 }, { 0 }) + val v2 = failure.fold({ 1 }, { 0 }) + + assertThat("v1 is equal 1", v1, equalTo(1)) + assertThat("v2 is equal 1", v2, equalTo(0)) + } + + //helper + private fun Nothing.count() = 0 + + @Test + fun map() { + val success = Result.of("success") + val failure = Result.error(RuntimeException("failure")) + + val v1 = success.map { it.count() } + val v2 = failure.map { it.count() } + + assertThat("v1 getAsInt equals 7", v1.getAs(), equalTo(7)) + assertThat("v2 getAsInt null", v2.getAs(), nullValue()) + } + + @Test + fun flatMap() { + val success = Result.of("success") + val failure = Result.error(RuntimeException("failure")) + + val v1 = success.flatMap { Result.of(it.last()) } + val v2 = failure.flatMap { Result.of(it.count()) } + + assertThat("v1 getAsChar equals s", v1.getAs(), equalTo('s')) + assertThat("v2 getAsInt null", v2.getAs(), nullValue()) + } + + @Test + fun mapError() { + val success = Result.of("success") + val failure = Result.error(Exception("failure")) + + val v1 = success.mapError { InstantiationException(it.message) } + val v2 = failure.mapError { InstantiationException(it.message) } + + assertThat("v1 is success", v1, instanceOf(Result.Success::class.java)) + assertThat("v1 is success", v1.component1(), equalTo("success")) + assertThat("v2 is failure", v2, instanceOf(Result.Failure::class.java)) + assertThat("v2 is failure", v2.component2()!!.message, equalTo("failure")) + } + + @Test + fun flatMapError() { + val success = Result.of("success") + val failure = Result.error(Exception("failure")) + + val v1 = success.flatMapError { Result.error(IllegalArgumentException()) } + val v2 = failure.flatMapError { Result.error(IllegalArgumentException()) } + + + assertThat("v1 is success", v1, instanceOf(Result.Success::class.java)) + assertThat("v1 is success", v1.getAs(), equalTo("success")) + assertThat("v2 is failure", v2, instanceOf(Result.Failure::class.java)) + assertThat("v2 is failure", v2.component2() is IllegalArgumentException, equalTo(true)) + } + + @Test + @Ignore + fun any() { + val foo = Result.of { readFromAssetFileName("foo.txt") } + val fooo = Result.of { readFromAssetFileName("fooo.txt") } + + val v1 = foo.any { "Lorem" in it } + val v2 = fooo.any { "Lorem" in it } + val v3 = foo.any { "LOREM" in it } + + assertThat(v1, equalTo(true)) + assertThat(v2, equalTo(false)) + assertThat(v3, equalTo(false)) + } + + @Test + fun anyWithThrow() { + val foo = Result.of { readFromAssetFileName("foo.txt") } + + val v1 = foo.any { "Lorem" in it } + val v2 = foo.any { readFromAssetFileName("ff.txt"); true } + + assertThat(v1, equalTo(true)) + assertThat(v2, equalTo(false)) + } + + @Test + @Ignore + fun composableFunctions1() { + val foo = { readFromAssetFileName("foo.txt") } + val bar = { readFromAssetFileName("bar.txt") } + + val notFound = { readFromAssetFileName("fooo.txt") } + + val (value1, error1) = Result.of(foo).map { it.count() }.mapError { IllegalStateException() } + val (value2, error2) = Result.of(notFound).map { bar }.mapError { IllegalStateException() } + + assertThat("value1 is 574", value1, equalTo(574)) + assertThat("error1 is null", error1, nullValue()) + assertThat("value2 is null", value2, nullValue()) + assertThat("error2 is Exception", error2 is IllegalStateException, equalTo(true)) + } + + @Test + fun composableFunctions2() { + val r1 = Result.of(functionThatCanReturnNull(false)).flatMap { resultReadFromAssetFileName("bar.txt") }.mapError { Exception("this should not happen") } + val r2 = Result.of(functionThatCanReturnNull(true)).map { it.rangeTo(Int.MAX_VALUE) }.mapError { KotlinNullPointerException() } + + assertThat("r1 is Result.Success type", r1, instanceOf(Result.Success::class.java)) + assertThat("r2 is Result.Failure type", r2, instanceOf(Result.Failure::class.java)) + } + + @Test + fun noException() { + val r = concat("1", "2") + assertThat("r is Result.Success type", r, instanceOf(Result.Success::class.java)) + } + + @Test + fun fanoutSuccesses() { + val readFooResult = resultReadFromAssetFileName("foo.txt") + val readBarResult = resultReadFromAssetFileName("bar.txt") + + val finalResult = readFooResult.zip { readBarResult } + val (v, e) = finalResult + + assertThat("finalResult is success", finalResult, instanceOf(Result.Success::class.java)) + assertThat("finalResult has a pair type when both are successes", v is Pair, equalTo(true)) + assertThat("value of finalResult has text from foo as left and text from bar as right", + v!!.first.startsWith("Lorem Ipsum is simply dummy text") && v.second.startsWith("Contrary to popular belief"), equalTo(true)) + assertThat("value of the second component is null", e, nullValue()) + } + + @Test + @Ignore + fun fanoutFailureOnLeft() { + val readFoooResult = resultReadFromAssetFileName("fooo.txt") + val readBarResult = resultReadFromAssetFileName("bar.txt") + + val finalResult = readFoooResult.zip { readBarResult } + val (v, e) = finalResult + + assertThat("value of the first component is null", v, nullValue()) + assertThat("finalResult is failure", finalResult, instanceOf(Result.Failure::class.java)) + assertThat("error is a file not found exception", e, instanceOf(FileNotFoundException::class.java)) + } + + @Test + @Ignore + fun fanoutFailureOnRight() { + val readFoooResult = resultReadFromAssetFileName("foo.txt") + val readBarResult = resultReadFromAssetFileName("barr.txt") + + val finalResult = readFoooResult.zip { readBarResult } + val (v, e) = finalResult + + assertThat("value of the first component is null", v, nullValue()) + assertThat("finalResult is failure", finalResult, instanceOf(Result.Failure::class.java)) + assertThat("error is a file not found exception", e, instanceOf(FileNotFoundException::class.java)) + } + + @Test(expected = SampleException::class) + fun mapThatThrows() { + val result = Result.of(1) + + var isCalled = false + + val newResult = result.map { throws() } + newResult.failure { isCalled = true } + + assertThat("newResult is transformed into failure", newResult, instanceOf(Result.Failure::class.java)) + assertThat("isCalled is being set as true", isCalled, equalTo(true)) + } + + @Test(expected = SampleException::class) + fun flatMapThatThrows() { + val result = Result.of("hello") + + var isCalled = false + + val newResult = result.flatMap { throwsForFlatmap() } + newResult.failure { isCalled = true } + + assertThat("newResult is transformed into failure", newResult, instanceOf(Result.Failure::class.java)) + assertThat("isCalled is being set as true", isCalled, equalTo(true)) + } + + @Test + fun successIsSubtypeOfResult() { + class AlwaysSuccess : GetFoo { + override fun foo(): Result = Result.success(Foo) + } + + val s = AlwaysSuccess() + + assertThat(s.foo(), instanceOf(Result::class.java)) + assertThat(s.foo().get(), equalTo(Foo)) + } + + @Test(expected = IllegalAccessException::class) + fun failureIsSubtypeOfResult() { + class AlwaysFailure : GetFoo { + override fun foo(): Result = Result.error(IllegalAccessException("Can't get foo")) + } + + val e = AlwaysFailure() + + assertThat(e.foo(), instanceOf(Result::class.java)) + + e.foo().get() + } + + @Test + fun liftListToResultOfListSuccess() { + val rs = listOf("bar", "foo").map { "$it.txt" }.map { resultReadFromAssetFileName(it) }.sequence() + + assertThat(rs, instanceOf(Result::class.java)) + assertThat(rs, instanceOf(Result.Success::class.java)) + assertThat(rs.get()[0], equalTo(readFromAssetFileName("bar.txt"))) + } + + @Test + @Ignore + fun liftListToResultOfListFailure() { + val rs = listOf("bar", "not_found").map { "$it.txt" }.map { resultReadFromAssetFileName(it) }.sequence() + + assertThat(rs, instanceOf(Result::class.java)) + assertThat(rs, instanceOf(Result.Failure::class.java)) + val (_, error) = rs + assertThat(error, instanceOf(FileNotFoundException::class.java)) + } + + object Foo + + interface GetFoo { + fun foo(): Result + } + + // helper + private fun readFromAssetFileName(name: String): String { + val dir = System.getProperty("user.dir") + val assetsDir = File(dir, "src/test/assets/") + return File(assetsDir, name).readText() + } + + private fun resultReadFromAssetFileName(name: String): Result { + val operation = { readFromAssetFileName(name) } + return Result.of(operation) + } + + private fun functionThatCanReturnNull(nullEnabled: Boolean): Int? = if (nullEnabled) null else Int.MIN_VALUE + + private fun concat(a: String, b: String): Result = Result.Success(a + b) + + private fun throws() { + throw SampleException("") + } + + private fun throwsForFlatmap(): Result { + throws() + return Result.of(1) + } +} diff --git a/result/src/test/kotlin/com/virtuslab/basetypes/result/SampleException.kt b/result/src/test/kotlin/com/virtuslab/basetypes/result/SampleException.kt new file mode 100644 index 0000000..a645292 --- /dev/null +++ b/result/src/test/kotlin/com/virtuslab/basetypes/result/SampleException.kt @@ -0,0 +1,5 @@ +package com.virtuslab.basetypes.result + +import java.lang.RuntimeException + +class SampleException(message: String): RuntimeException(message) \ No newline at end of file diff --git a/result/src/test/kotlin/com/virtuslab/basetypes/result/ValidationTests.kt b/result/src/test/kotlin/com/virtuslab/basetypes/result/ValidationTests.kt new file mode 100644 index 0000000..f6fbea0 --- /dev/null +++ b/result/src/test/kotlin/com/virtuslab/basetypes/result/ValidationTests.kt @@ -0,0 +1,33 @@ +package com.virtuslab.basetypes.result + +import org.junit.Assert.assertThat +import org.junit.Test +import org.hamcrest.CoreMatchers.`is` as isEqualTo + +class ValidationTests { + + @Test + fun testValidation() { + val r1: Result = Result.of(1) + val r2: Result = Result.of(2) + val r3: Result = Result.of(3) + + val validation = Validation(r1, r2, r3) + assertThat("validation.hasFailures", validation.hasFailure, isEqualTo(false)) + assertThat("validation.failures", validation.failures, isEqualTo(listOf())) + } + + @Test(expected = SampleException::class) + fun testValidationWithError() { + + val r1: Result = Result.of(1) + val r2: Result = Result.of { throw SampleException("Not a number") } + val r3: Result = Result.of(3) + val r4: Result = Result.of { throw SampleException("Division by zero") } + + val validation = Validation(r1, r2, r3, r4) + assertThat("validation.hasFailures", validation.hasFailure, isEqualTo(true)) + assertThat("validation.failures", validation.failures.map { it.message }, isEqualTo(listOf("Not a number", "Division by zero"))) + } + +} diff --git a/settings.gradle.kts b/settings.gradle.kts index 8a791d6..8621b7f 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1 +1 @@ -include(":result-rxjava", ":result-reactor", ":result-arrow", ":refined-types") +include(":result", ":result-rxjava", ":result-reactor", ":result-arrow", ":refined-types")