From ee8e80f36a7cb4c2aaeb4bc2ecfee96e8f9045ef Mon Sep 17 00:00:00 2001 From: Evan Sarkar Date: Mon, 29 Jun 2026 01:02:04 +0530 Subject: [PATCH 1/2] feat(core): add isOk / isError / getOrNull / getOrThrow to Fat12Result MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add idiomatic Kotlin convenience accessors to the sealed Fat12Result so callers can branch on success without an exhaustive when or an is Fat12Result.Ok cast every time: - isOk / isError — boolean success check - getOrNull() — success value, or null for any non-Ok outcome - getOrThrow() — success value, or IllegalStateException describing it Additive and non-breaking; the existing sealed subclasses are unchanged. Adds Fat12ResultAccessorsTest covering Ok (incl. an Ok wrapping null) and the NotFound / DiskFull non-Ok variants. Fixes #3 --- .../com/ams/fat12ex/core/Fat12Result.kt | 20 +++++++ .../fat12ex/core/Fat12ResultAccessorsTest.kt | 56 +++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 core/src/test/kotlin/com/ams/fat12ex/core/Fat12ResultAccessorsTest.kt diff --git a/core/src/main/kotlin/com/ams/fat12ex/core/Fat12Result.kt b/core/src/main/kotlin/com/ams/fat12ex/core/Fat12Result.kt index c70d2da..40d8bce 100644 --- a/core/src/main/kotlin/com/ams/fat12ex/core/Fat12Result.kt +++ b/core/src/main/kotlin/com/ams/fat12ex/core/Fat12Result.kt @@ -17,3 +17,23 @@ sealed class Fat12Result { data class NotFound(val path: String) : Fat12Result() data class InvalidName(val name: String, val reason: String) : Fat12Result() } + +/** True when this result is [Fat12Result.Ok]. */ +val Fat12Result<*>.isOk: Boolean get() = this is Fat12Result.Ok + +/** True when this result is any non-[Fat12Result.Ok] outcome. */ +val Fat12Result<*>.isError: Boolean get() = this !is Fat12Result.Ok + +/** The success value, or `null` for any non-[Fat12Result.Ok] outcome. */ +fun Fat12Result.getOrNull(): T? = (this as? Fat12Result.Ok)?.value + +/** + * The success value, or throw [IllegalStateException] describing the non-[Fat12Result.Ok] + * outcome. Use only when a non-Ok result is a programming error at the call site; prefer + * [getOrNull] or an exhaustive `when` for recoverable handling. + */ +fun Fat12Result.getOrThrow(): T = + when (this) { + is Fat12Result.Ok -> value + else -> error("Fat12Result was not Ok: $this") + } diff --git a/core/src/test/kotlin/com/ams/fat12ex/core/Fat12ResultAccessorsTest.kt b/core/src/test/kotlin/com/ams/fat12ex/core/Fat12ResultAccessorsTest.kt new file mode 100644 index 0000000..5845f6a --- /dev/null +++ b/core/src/test/kotlin/com/ams/fat12ex/core/Fat12ResultAccessorsTest.kt @@ -0,0 +1,56 @@ +package com.ams.fat12ex.core + +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertFalse +import org.junit.jupiter.api.Assertions.assertNull +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows + +/** + * [Fat12Result] convenience-accessor tests — [isOk] / [isError] / [getOrNull] / + * [getOrThrow]. + * + * Proves the accessors branch on success without an exhaustive `when` or an + * `is Fat12Result.Ok` cast, for both the [Fat12Result.Ok] case and a couple of + * representative non-Ok variants ([Fat12Result.NotFound], [Fat12Result.DiskFull]). + */ +class Fat12ResultAccessorsTest { + + @Test + fun ok_isOk_getOrNull_getOrThrow() { + val result: Fat12Result = Fat12Result.Ok("payload") + assertTrue(result.isOk) + assertFalse(result.isError) + assertEquals("payload", result.getOrNull()) + assertEquals("payload", result.getOrThrow()) + } + + @Test + fun notFound_isNotOk_getOrNull_null_getOrThrow_throws() { + val result: Fat12Result = Fat12Result.NotFound("/MISSING.TXT") + assertFalse(result.isOk) + assertTrue(result.isError) + assertNull(result.getOrNull()) + assertThrows { result.getOrThrow() } + } + + @Test + fun diskFull_isNotOk_getOrNull_null_getOrThrow_throws() { + val result: Fat12Result = Fat12Result.DiskFull + assertFalse(result.isOk) + assertTrue(result.isError) + assertNull(result.getOrNull()) + assertThrows { result.getOrThrow() } + } + + @Test + fun ok_withNullableValue_getOrNull_distinguishesNullValueFromError() { + // An Ok wrapping a null value still reports isOk; getOrNull returns null here + // too, but getOrThrow returns the (null) value rather than throwing. + val result: Fat12Result = Fat12Result.Ok(null) + assertTrue(result.isOk) + assertNull(result.getOrNull()) + assertNull(result.getOrThrow()) + } +} From ab4a008ff911152884832c2b3b3c004c821f9d00 Mon Sep 17 00:00:00 2001 From: Evan Sarkar Date: Mon, 29 Jun 2026 02:39:12 +0530 Subject: [PATCH 2/2] fix(core): give Fat12Result.DiskFull a stable toString for getOrThrow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit DiskFull is a singleton object, so it inherited the default Foo@hash toString — meaning getOrThrow() on a DiskFull produced an unreadable message like 'Fat12Result was not Ok: ...DiskFull@7a3b' rather than describing the outcome as the KDoc promises. Override toString() to return 'DiskFull' and assert the getOrThrow message (not just the exception type) in the NotFound / DiskFull test cases. Addresses CodeRabbit review feedback on this PR. --- .../kotlin/com/ams/fat12ex/core/Fat12Result.kt | 6 +++++- .../ams/fat12ex/core/Fat12ResultAccessorsTest.kt | 14 ++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/core/src/main/kotlin/com/ams/fat12ex/core/Fat12Result.kt b/core/src/main/kotlin/com/ams/fat12ex/core/Fat12Result.kt index 40d8bce..b8e1447 100644 --- a/core/src/main/kotlin/com/ams/fat12ex/core/Fat12Result.kt +++ b/core/src/main/kotlin/com/ams/fat12ex/core/Fat12Result.kt @@ -12,7 +12,11 @@ package com.ams.fat12ex.core sealed class Fat12Result { data class Ok(val value: T) : Fat12Result() data class NameConflict(val name: String) : Fat12Result() - object DiskFull : Fat12Result() + object DiskFull : Fat12Result() { + // Stable, human-readable rendering (an `object` otherwise inherits the + // Foo@hash default) so getOrThrow()'s message reads "...not Ok: DiskFull". + override fun toString(): String = "DiskFull" + } data class TooLarge(val actualBytes: Long) : Fat12Result() data class NotFound(val path: String) : Fat12Result() data class InvalidName(val name: String, val reason: String) : Fat12Result() diff --git a/core/src/test/kotlin/com/ams/fat12ex/core/Fat12ResultAccessorsTest.kt b/core/src/test/kotlin/com/ams/fat12ex/core/Fat12ResultAccessorsTest.kt index 5845f6a..acfd971 100644 --- a/core/src/test/kotlin/com/ams/fat12ex/core/Fat12ResultAccessorsTest.kt +++ b/core/src/test/kotlin/com/ams/fat12ex/core/Fat12ResultAccessorsTest.kt @@ -32,7 +32,11 @@ class Fat12ResultAccessorsTest { assertFalse(result.isOk) assertTrue(result.isError) assertNull(result.getOrNull()) - assertThrows { result.getOrThrow() } + val ex = assertThrows { result.getOrThrow() } + assertTrue( + ex.message?.contains("NotFound(path=/MISSING.TXT)") == true, + "message must describe the non-Ok outcome, was: ${ex.message}", + ) } @Test @@ -41,7 +45,13 @@ class Fat12ResultAccessorsTest { assertFalse(result.isOk) assertTrue(result.isError) assertNull(result.getOrNull()) - assertThrows { result.getOrThrow() } + val ex = assertThrows { result.getOrThrow() } + // DiskFull is a singleton object; its message must be the stable "DiskFull", + // not the inherited Foo@hash default. + assertTrue( + ex.message?.contains("DiskFull") == true && ex.message?.contains("@") != true, + "message must read 'DiskFull', was: ${ex.message}", + ) } @Test