From bffa455ee4f9d757c9e5b5052d1282b3def04822 Mon Sep 17 00:00:00 2001 From: Michael Bull Date: Sat, 21 Oct 2017 16:52:29 +0100 Subject: [PATCH] Add feature parity with Rust's Result type --- src/main/kotlin/com/mikebull94/result/And.kt | 15 +++- src/main/kotlin/com/mikebull94/result/Get.kt | 27 +++++++- src/main/kotlin/com/mikebull94/result/Map.kt | 6 +- src/main/kotlin/com/mikebull94/result/Or.kt | 17 +++-- .../kotlin/com/mikebull94/result/Result.kt | 1 + .../com/mikebull94/result/ResultIterator.kt | 43 ++++++++++++ .../kotlin/com/mikebull94/result/Unwrap.kt | 43 ++++++++++++ .../kotlin/com/mikebull94/result/AndTest.kt | 19 +++-- .../kotlin/com/mikebull94/result/GetTest.kt | 32 +++++++-- .../kotlin/com/mikebull94/result/OrTest.kt | 16 ++--- .../mikebull94/result/ResultIteratorTest.kt | 69 +++++++++++++++++++ .../com/mikebull94/result/UnwrapTest.kt | 60 ++++++++++++++++ 12 files changed, 318 insertions(+), 30 deletions(-) create mode 100644 src/main/kotlin/com/mikebull94/result/ResultIterator.kt create mode 100644 src/main/kotlin/com/mikebull94/result/Unwrap.kt create mode 100644 src/test/kotlin/com/mikebull94/result/ResultIteratorTest.kt create mode 100644 src/test/kotlin/com/mikebull94/result/UnwrapTest.kt diff --git a/src/main/kotlin/com/mikebull94/result/And.kt b/src/main/kotlin/com/mikebull94/result/And.kt index bddbdcb..c05f7d8 100644 --- a/src/main/kotlin/com/mikebull94/result/And.kt +++ b/src/main/kotlin/com/mikebull94/result/And.kt @@ -1,9 +1,20 @@ package com.mikebull94.result /** - * - Elm: [Result.andThen](http://package.elm-lang.org/packages/elm-lang/core/latest/Result#andThen) + * Rust: [Result.and](https://doc.rust-lang.org/std/result/enum.Result.html#method.and) */ -inline fun Result.andThen(transform: (V) -> Result): Result { +infix fun Result.and(result: Result): Result { + return when(this) { + is Ok -> result + is Error -> err(error) + } +} + +/** + * - Elm: [Result.andThen](http://package.elm-lang.org/packages/elm-lang/core/latest/Result#andThen) + * - Rust: [Result.and_then](https://doc.rust-lang.org/std/result/enum.Result.html#method.and_then) + */ +infix inline fun Result.andThen(transform: (V) -> Result): Result { return when (this) { is Ok -> transform(value) is Error -> err(error) diff --git a/src/main/kotlin/com/mikebull94/result/Get.kt b/src/main/kotlin/com/mikebull94/result/Get.kt index 3134c4d..5387022 100644 --- a/src/main/kotlin/com/mikebull94/result/Get.kt +++ b/src/main/kotlin/com/mikebull94/result/Get.kt @@ -2,6 +2,7 @@ package com.mikebull94.result /** * - Elm: [Result.toMaybe](http://package.elm-lang.org/packages/elm-lang/core/latest/Result#toMaybe) + * - Rust: [Result.ok](https://doc.rust-lang.org/std/result/enum.Result.html#method.ok) */ fun Result.get(): V? { return when (this) { @@ -11,11 +12,33 @@ fun Result.get(): V? { } /** - * - Elm: [Result.withDefault](http://package.elm-lang.org/packages/elm-lang/core/latest/Result#withDefault) + * - Rust: [Result.err](https://doc.rust-lang.org/std/result/enum.Result.html#method.err) */ -infix fun Result.getOrElse(default: V): V { +fun Result.getError(): E? { + return when(this) { + is Ok -> null + is Error -> error + } +} + +/** + * - Elm: [Result.withDefault](http://package.elm-lang.org/packages/elm-lang/core/latest/Result#withDefault) + * - Rust: [Result.unwrap_or](https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap_or) + */ +infix fun Result.getOr(default: V): V { return when (this) { is Ok -> value is Error -> default } } + +/** + * - Elm: [Result.extract](http://package.elm-lang.org/packages/circuithub/elm-result-extra/1.4.0/Result-Extra#extract) + * - Rust: [Result.unwrap_or_else](https://doc.rust-lang.org/src/core/result.rs.html#735-740) + */ +infix inline fun Result.getOrElse(transform: (E) -> V): V { + return when (this) { + is Ok -> value + is Error -> transform(error) + } +} diff --git a/src/main/kotlin/com/mikebull94/result/Map.kt b/src/main/kotlin/com/mikebull94/result/Map.kt index dbba0a9..856e225 100644 --- a/src/main/kotlin/com/mikebull94/result/Map.kt +++ b/src/main/kotlin/com/mikebull94/result/Map.kt @@ -3,8 +3,9 @@ package com.mikebull94.result /** * - Elm: [Result.map](http://package.elm-lang.org/packages/elm-lang/core/latest/Result#map) * - Haskell: [Data.Bifunctor.first](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Bifunctor.html#v:first) + * - Rust: [Result.map](https://doc.rust-lang.org/std/result/enum.Result.html#method.map) */ -inline fun Result.map(transform: (V) -> U): Result { +infix inline fun Result.map(transform: (V) -> U): Result { return when (this) { is Ok -> ok(transform(value)) is Error -> err(error) @@ -14,8 +15,9 @@ inline fun Result.map(transform: (V) -> U): Result { /** * - Elm: [Result.mapError](http://package.elm-lang.org/packages/elm-lang/core/latest/Result#mapError) * - Haskell: [Data.Bifunctor.right](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Bifunctor.html#v:second) + * - Rust: [Result.map_err](https://doc.rust-lang.org/std/result/enum.Result.html#method.map_err) */ -inline fun Result.mapError(transform: (E) -> U): Result { +infix inline fun Result.mapError(transform: (E) -> U): Result { return when (this) { is Ok -> ok(value) is Error -> err(transform(error)) diff --git a/src/main/kotlin/com/mikebull94/result/Or.kt b/src/main/kotlin/com/mikebull94/result/Or.kt index c5fee17..a5c134c 100644 --- a/src/main/kotlin/com/mikebull94/result/Or.kt +++ b/src/main/kotlin/com/mikebull94/result/Or.kt @@ -1,18 +1,21 @@ package com.mikebull94.result -infix fun Result.or(default: V): Result { - return when (this) { - is Ok -> this - is Error -> ok(default) +/** + * - Rust: [Result.or](https://doc.rust-lang.org/std/result/enum.Result.html#method.or) + */ +infix fun Result.or(result: Result): Result { + return when(this) { + is Ok -> ok(value) + is Error -> result } } /** - * - Elm: [Result.extract](http://package.elm-lang.org/packages/circuithub/elm-result-extra/1.4.0/Result-Extra#extract) + * - Rust: [Result.or_else](https://doc.rust-lang.org/std/result/enum.Result.html#method.or_else) */ -inline fun Result.extract(transform: (E) -> V): V { +infix inline fun Result.orElse(transform: (E) -> Result): Result { return when (this) { - is Ok -> value + is Ok -> ok(this.value) is Error -> transform(error) } } diff --git a/src/main/kotlin/com/mikebull94/result/Result.kt b/src/main/kotlin/com/mikebull94/result/Result.kt index a91ad78..1733d46 100644 --- a/src/main/kotlin/com/mikebull94/result/Result.kt +++ b/src/main/kotlin/com/mikebull94/result/Result.kt @@ -3,6 +3,7 @@ package com.mikebull94.result /** * - Elm: [Result](http://package.elm-lang.org/packages/elm-lang/core/5.1.1/Result) * - Haskell: [Data.Either](https://hackage.haskell.org/package/base-4.10.0.0/docs/Data-Either.html) + * - Rust: [Result](https://doc.rust-lang.org/std/result/enum.Result.html) */ sealed class Result diff --git a/src/main/kotlin/com/mikebull94/result/ResultIterator.kt b/src/main/kotlin/com/mikebull94/result/ResultIterator.kt new file mode 100644 index 0000000..a16da93 --- /dev/null +++ b/src/main/kotlin/com/mikebull94/result/ResultIterator.kt @@ -0,0 +1,43 @@ +package com.mikebull94.result + +/** + * - Rust: [Result.iter](https://doc.rust-lang.org/std/result/enum.Result.html#method.iter) + */ +fun Result.iterator(): Iterator { + return ResultIterator(this) +} + +/** + * Rust: [Result.iter_mut](https://doc.rust-lang.org/std/result/enum.Result.html#method.iter_mut) + */ +fun Result.mutableIterator(): MutableIterator { + return ResultIterator(this) +} + +private class ResultIterator(private val result: Result) : MutableIterator { + private var yielded = false + + override fun hasNext(): Boolean { + if (yielded) { + return false + } + + return when (result) { + is Ok -> true + is Error -> false + } + } + + override fun next(): V { + if (!yielded && result is Ok) { + yielded = true + return result.value + } else { + throw NoSuchElementException() + } + } + + override fun remove() { + yielded = true + } +} diff --git a/src/main/kotlin/com/mikebull94/result/Unwrap.kt b/src/main/kotlin/com/mikebull94/result/Unwrap.kt new file mode 100644 index 0000000..16333d0 --- /dev/null +++ b/src/main/kotlin/com/mikebull94/result/Unwrap.kt @@ -0,0 +1,43 @@ +package com.mikebull94.result + +class UnwrapException(message: String): Exception(message) + +/** + * - Rust: [Result.unwrap](https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap) + */ +fun Result.unwrap(): V { + return when (this) { + is Ok -> value + is Error -> throw UnwrapException("called Result.wrap on an Error value $error") + } +} + +/** + * - Rust: [Result.expect](https://doc.rust-lang.org/std/result/enum.Result.html#method.expect) + */ +infix fun Result.expect(message: String): V { + return when (this) { + is Ok -> value + is Error -> throw UnwrapException(message) + } +} + +/** + * - Rust: [Result.unwrap_err](https://doc.rust-lang.org/std/result/enum.Result.html#method.unwrap_err) + */ +fun Result.unwrapError(): E { + return when (this) { + is Ok -> throw UnwrapException("called Result.unwrapError on an Ok value $value") + is Error -> error + } +} + +/** + * - Rust: [Reseult.expect_err](https://doc.rust-lang.org/std/result/enum.Result.html#method.expect_err) + */ +infix fun Result.expectError(message: String): E { + return when (this) { + is Ok -> throw UnwrapException(message) + is Error -> error + } +} diff --git a/src/test/kotlin/com/mikebull94/result/AndTest.kt b/src/test/kotlin/com/mikebull94/result/AndTest.kt index e65acec..61d30ad 100644 --- a/src/test/kotlin/com/mikebull94/result/AndTest.kt +++ b/src/test/kotlin/com/mikebull94/result/AndTest.kt @@ -8,6 +8,18 @@ import org.junit.jupiter.api.Test internal class AndTest { private object AndError + @Test + internal fun `and should return the result value if ok`() { + val value = ok(230).and(ok(500)).get() + assertThat(value, equalTo(500)) + } + + @Test + internal fun `and should return the result value if not ok`() { + val error = ok(300).and(err("hello world")).getError() + assertThat(error, equalTo("hello world")) + } + @Test internal fun `andThen should return the transformed result value if ok`() { val value = ok(5).andThen { ok(it + 7) }.get() @@ -16,10 +28,7 @@ internal class AndTest { @Test internal fun `andThen should return the result error if not ok`() { - val result = ok(20).andThen { ok(it + 43) }.andThen { err(AndError) } - - result as Error - - assertThat(result.error, sameInstance(AndError)) + val error = ok(20).andThen { ok(it + 43) }.andThen { err(AndError) }.getError()!! + assertThat(error, sameInstance(AndError)) } } diff --git a/src/test/kotlin/com/mikebull94/result/GetTest.kt b/src/test/kotlin/com/mikebull94/result/GetTest.kt index 4b8e754..856ba5f 100644 --- a/src/test/kotlin/com/mikebull94/result/GetTest.kt +++ b/src/test/kotlin/com/mikebull94/result/GetTest.kt @@ -20,14 +20,38 @@ internal class GetTest { } @Test - internal fun `getOrElse should return the result value if ok`() { - val value = ok("hello").getOrElse("world") + internal fun `getError should return null if ok`() { + val error = ok("example").getError() + assertThat(error, equalTo(null)) + } + + @Test + internal fun `getError should return the result error if not ok`() { + val error = err("example").getError() + assertThat(error, equalTo("example")) + } + + @Test + internal fun `getOr should return the result value if ok`() { + val value = ok("hello").getOr("world") assertThat(value, equalTo("hello")) } @Test - internal fun `getOrElse should return default value if not ok`() { - val value = err(GetError).getOrElse("default") + internal fun `getOr should return default value if not ok`() { + val value = err(GetError).getOr("default") assertThat(value, equalTo("default")) } + + @Test + internal fun `getOrElse should return the result value if ok`() { + val value = ok("hello").getOrElse { "world" } + assertThat(value, equalTo("hello")) + } + + @Test + internal fun `getOrElse should return the transformed result error if ok`() { + val value = err("hello").getOrElse { "world" } + assertThat(value, equalTo("world")) + } } diff --git a/src/test/kotlin/com/mikebull94/result/OrTest.kt b/src/test/kotlin/com/mikebull94/result/OrTest.kt index 3307e82..f25cd96 100644 --- a/src/test/kotlin/com/mikebull94/result/OrTest.kt +++ b/src/test/kotlin/com/mikebull94/result/OrTest.kt @@ -9,25 +9,25 @@ internal class OrTest { @Test internal fun `or should return the result value if ok`() { - val value = ok(500).or(1000).get() + val value = ok(500).or(ok(1000)).get() assertThat(value, equalTo(500)) } @Test internal fun `or should return the default value if not ok`() { - val error = err(OrError).or(5000).get() + val error = err(OrError).or(ok(5000)).get() assertThat(error, equalTo(5000)) } @Test - internal fun `extract should return the result value if ok`() { - val value = ok("hello").extract { OrError } as String - assertThat(value, equalTo("hello")) + internal fun `orElse should return the result value if ok`() { + val value = ok(3000).orElse { ok(4000) }.get() + assertThat(value, equalTo(3000)) } @Test - internal fun `extract should return the transformed result error if not ok`() { - val error = err("hello").extract { "$it darkness" } - assertThat(error, equalTo("hello darkness")) + internal fun `orElse should return the transformed value if not ok`() { + val error = err(4000).orElse { ok(2000) }.get() + assertThat(error, equalTo(2000)) } } diff --git a/src/test/kotlin/com/mikebull94/result/ResultIteratorTest.kt b/src/test/kotlin/com/mikebull94/result/ResultIteratorTest.kt new file mode 100644 index 0000000..72763bb --- /dev/null +++ b/src/test/kotlin/com/mikebull94/result/ResultIteratorTest.kt @@ -0,0 +1,69 @@ +package com.mikebull94.result + +import com.natpryce.hamkrest.assertion.assertThat +import com.natpryce.hamkrest.equalTo +import org.junit.jupiter.api.Assertions.* +import org.junit.jupiter.api.Test + +internal class ResultIteratorTest { + @Test + internal fun `hasNext should return true if unyielded and result is ok`() { + val iterator = ok("hello").iterator() + assertThat(iterator.hasNext(), equalTo(true)) + } + + @Test + internal fun `hasNext should return false if result is not ok`() { + val iterator = err("hello").iterator() + assertThat(iterator.hasNext(), equalTo(false)) + } + + @Test + internal fun `hasNext should return false if yielded`() { + val iterator = ok("hello").iterator() + iterator.next() + assertThat(iterator.hasNext(), equalTo(false)) + } + + @Test + internal fun `next should return the result value if unyielded and result is ok`() { + val value = ok("hello").iterator().next() + assertThat(value, equalTo("hello")) + } + + @Test + internal fun `next should throw NoSuchElementException if unyielded and result is not ok`() { + val iterator = err("hello").iterator() + + assertThrows(NoSuchElementException::class.java) { + iterator.next() + } + } + + @Test + internal fun `next should throw NoSuchElementException if yielded and result is ok`() { + val iterator = ok("hello").iterator() + iterator.next() + + assertThrows(NoSuchElementException::class.java) { + iterator.next() + } + } + + @Test + internal fun `remove should make hasNext return false`() { + val iterator = ok("hello").mutableIterator() + iterator.remove() + assertThat(iterator.hasNext(), equalTo(false)) + } + + @Test + internal fun `remove should make next throw NoSuchElementException`() { + val iterator = ok("hello").mutableIterator() + iterator.remove() + + assertThrows(NoSuchElementException::class.java) { + iterator.next() + } + } +} diff --git a/src/test/kotlin/com/mikebull94/result/UnwrapTest.kt b/src/test/kotlin/com/mikebull94/result/UnwrapTest.kt new file mode 100644 index 0000000..bc9623e --- /dev/null +++ b/src/test/kotlin/com/mikebull94/result/UnwrapTest.kt @@ -0,0 +1,60 @@ +package com.mikebull94.result + +import com.natpryce.hamkrest.assertion.assertThat +import com.natpryce.hamkrest.equalTo +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test + +internal class UnwrapTest { + @Test + internal fun `unwrap should return the result value if ok`() { + val value = ok(5000).unwrap() + assertThat(value, equalTo(5000)) + } + + @Test + internal fun `unwrap should throw an UnwrapException if not ok`() { + assertThrows(UnwrapException::class.java, { + err(5000).unwrap() + }, "called Result.wrap on an Error value 5000") + } + + @Test + internal fun `expect should return the result value if ok`() { + val value = ok(1994).expect("the year should be 1994") + assertThat(value, equalTo(1994)) + } + + @Test + internal fun `expect should throw an UnwrapException with a specified message if not ok`() { + assertThrows(UnwrapException::class.java, { + err(1994).expect("the year should be 1994") + }, "the year should be 1994") + } + + @Test + internal fun `unwrapError should throw an UnwrapException if ok`() { + assertThrows(UnwrapException::class.java, { + ok("example").unwrapError() + }, "called Result.unwrapError on an Ok value example") + } + + @Test + internal fun `unwrapError should return the result error if not ok`() { + val error = err("example").unwrapError() + assertThat(error, equalTo("example")) + } + + @Test + internal fun `expectError should throw an UnwrapException with a specified message if ok`() { + assertThrows(UnwrapException::class.java, { + ok(2010).expectError("the year should be 2010") + }, "the year should be 2010") + } + + @Test + internal fun `expectError should return the result error if not ok`() { + val error = err(2010).expectError("the year should be 2010") + assertThat(error, equalTo(2010)) + } +}