Skip to content

Commit

Permalink
FailureException is now a failure
Browse files Browse the repository at this point in the history
  • Loading branch information
L-Briand committed Dec 27, 2023
1 parent 23a99bd commit 41d5d48
Show file tree
Hide file tree
Showing 8 changed files with 136 additions and 27 deletions.
60 changes: 59 additions & 1 deletion README.MD
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A Kotlin multiplatform, serializable, [Failure interface](src/commonMain/kotlin/net/orandja/failure/Failure.kt).

In almost every project, I use some form of error int or class to represent my code failures.
In almost every project, I use some form of error int or class to represent my code failures.
To not have to repeat myself every time I have created this lib.

## Import from maven
Expand Down Expand Up @@ -35,6 +35,8 @@ dependencies {

[dokka documentation here](https://l-briand.github.io/failure/failure/net.orandja.failure/index.html)

Usage example:

```kotlin
val TOO_LOW = failure("TOO_LOW", description = "Value is less than 0")
val TOO_HIGH by namedFailure(description = "Value is greater than 100")
Expand All @@ -46,4 +48,60 @@ fun assert(value: Int): Failure? {
}

assert(102)?.throws() // Exception in thread "main" FailureException: TOO_HIGH [Value is greater than 100] value is 102
```

### Exceptions

The default `Failure` is an interface.
You can implement it whenever it seems OK to do so.
For example, a failure that throws is also a failure.

```kotlin
val failure = failure("MY_FAILURE", code = 200)
try {
failure.throws()
} catch (e: FailureException) {
assert(e.id == failure.id)
assert(e.code == failure.code)
}
```

### Equality

To keep things simple and not implement your own failure class, you can use the `GenericFailure` data class.
This class is used when you create failures through `failure()` or `namedFailure()`

Keep in mind that the failure implementation can differ.
A `FailureException` is not equals to `GenericFailure`.
Either check for failure `id` equality or transforms them both to `GenericFailure` with the `defaults()` function.

```kotlin
val failure1: Failure = failure("FAILURE")
val failure2: FailureException = runCatching { failure1.throws() }.exceptionOrNull() !! as FailureException

// Proper checks
assert(failure1.id == failure2.id)
assert(failure1 == failure2.defaults()) // failure1 is a GenericFailure

// This fails
assert(failure1 == failure2)''
```

### Serialization

Any class that implements the `Failure` interface can use the `FailureSerializer` to serialize the failure.
For example, the `FailureException` is a `Failure`, it can be serialized but cannot be deserialized.

```kotlin
lateinit var failure = FailureException(failure("FAILURE"))
val json = Json.encodeToString<Failure>(failure)
println(json) // {"id":"FAILURE"}
```

Any serialized `Failure` can be decoded to a `GenericFailure`.

```kotlin
val json = """{"id":"FAILURE"}"""
val failure = Json.decodFromString<Failure>(json)
assert(failure.id == "FAILURE")
```
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import org.jetbrains.kotlin.gradle.targets.js.dsl.ExperimentalWasmDsl

plugins {
kotlin("multiplatform") version "1.9.21"
kotlin("plugin.serialization") version "1.9.21"
kotlin("multiplatform") version "1.9.22"
kotlin("plugin.serialization") version "1.9.22"
id("org.jetbrains.dokka") version "1.9.10"
id("maven-publish")
id("signing")
Expand Down
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ kotlin.code.style=official
kotlin.js.compiler=ir
# Global information
group=net.orandja.kt
version=1.1.0
version=1.1.1
# Dependencies
version.serialization=1.6.2
# Artifact related
Expand Down
4 changes: 2 additions & 2 deletions src/commonMain/kotlin/net/orandja/failure/Failure.kt
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ interface Failure {
/** An optional integer code associated with a failure. */
val code: Int?

/** A human-readable description of the failure. */
/** A human-readable description of the failure. Normally, this field never changes. */
val description: String?

/** Additional information about the failure. */
/** Additional information about the failure. Implementation can add anything related to the failure here. */
val information: String?

/** The underlying cause of the failure, if any. This field is not serialized. */
Expand Down
40 changes: 26 additions & 14 deletions src/commonMain/kotlin/net/orandja/failure/FailureException.kt
Original file line number Diff line number Diff line change
@@ -1,43 +1,55 @@
package net.orandja.failure

import kotlinx.serialization.Serializable


/**
* Exception class representing a Failure during code execution.
* This class is also a failure.
*
* @property failure The Failure instance associated with the exception.
*/
class FailureException(val failure: Failure) : Exception(buildString { buildMessage(failure, this) }, failure.cause) {
@Serializable(FailureSerializer::class)
class FailureException(
private val failure: Failure,
) : Exception(
buildString { buildMessage(failure) },
failure.cause
), Failure by failure {

companion object {
/**
* Builds the exception message based on the given [Failure].
*
* @param failure The Failure instance.
* @param builder The StringBuilder to append the message to. If not provided, a new StringBuilder will be created.
*/
private fun buildMessage(failure: Failure, builder: StringBuilder = StringBuilder(), prepend: String = "") {
private fun StringBuilder.buildMessage(failure: Failure, prepend: String = "") {
if (prepend.isNotEmpty()) {
builder.append('\n')
builder.append(prepend)
append('\n')
append(prepend)
}
builder.append(failure.id)
append(failure.id)
failure.code?.let {
builder.append(" [")
builder.append(it)
builder.append(']')
append(" [")
append(it)
append(']')
}
failure.description?.let {
builder.append(" (")
builder.append(it)
builder.append(')')
append(" (")
append(it)
append(')')
}
failure.information?.let {
builder.append(" ")
builder.append(it)
append(" ")
append(it)
}
val attached = failure.attached ?: return
attached.forEach {
buildMessage(it, builder, "$prepend> ")
buildMessage(it, "$prepend> ")
}
}
}

override val cause: Throwable? get() = failure.cause
}
22 changes: 15 additions & 7 deletions src/commonTest/kotlin/net/orandja/test/FailureException.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package net.orandja.test

import net.orandja.failure.Failure
import net.orandja.failure.FailureException
import net.orandja.failure.failure
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals

class FailureException {

Expand All @@ -24,23 +26,23 @@ class FailureException {
@Test
fun empty() {
val exception = catch { failureEmpty.throws() }
assertEquals(failureEmpty, exception.failure)
assertFailureEquals(failureEmpty, exception)
assertEquals(TAG, exception.message)
assertEquals(null, exception.cause)
}

@Test
fun full() {
val exception = catch { failureFull.throws() }
assertEquals(failureFull, exception.failure)
assertFailureEquals(failureFull, exception)
assertEquals(null, exception.cause)
assertEquals("$TAG [$CODE] ($DESCRIPTION) $INFO", exception.message)
}

@Test
fun attach() {
val exception = catch { failureAttach1.throws() }
assertEquals(failureAttach1, exception.failure)
assertFailureEquals(failureAttach1, exception)
assertEquals(null, exception.cause)
val message = """
$TAG
Expand All @@ -53,7 +55,7 @@ class FailureException {
@Test
fun attach2() {
val exception = catch { failureAttach2.throws() }
assertEquals(failureAttach2, exception.failure)
assertFailureEquals(failureAttach2, exception)
assertEquals(null, exception.cause)
val message = """
$TAG
Expand All @@ -67,18 +69,24 @@ class FailureException {
@Test
fun exception() {
val exception = catch { failureException.throws() }
assertEquals(failureException, exception.failure)
assertFailureEquals(failureException, exception)
assertEquals(TAG, exception.message)
assertEquals(cause, exception.cause)
}

@Test
fun exceptionEquality() {
val failure2: FailureException = catch { failureEmpty.throws() }
assertEquals(failureEmpty.id, failure2.id)
assertEquals(failureEmpty, failure2.defaults())
assertNotEquals<Failure>(failureEmpty, failure2)
}

private fun catch(block: () -> Nothing): FailureException {
try {
block()
} catch (e: FailureException) {
return e
}
}


}
17 changes: 17 additions & 0 deletions src/commonTest/kotlin/net/orandja/test/FailureJson.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package net.orandja.test
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import net.orandja.failure.Failure
import net.orandja.failure.FailureException
import net.orandja.failure.failure
import kotlin.test.Test
import kotlin.test.assertEquals
Expand Down Expand Up @@ -35,10 +36,26 @@ class FailureJson {
assertEquals(jsonEmpty, codec.encodeToString(failureException))
}

@Test
fun serializeException() {
assertEquals(jsonEmpty, codec.encodeToString(catch { failureEmpty.throws() }))
assertEquals(jsonFull, codec.encodeToString(catch { failureFull.throws() }))
assertEquals(jsonWithAttach, codec.encodeToString(catch { failureAttach.throws() }))
assertEquals(jsonEmpty, codec.encodeToString(catch { failureException.throws() }))
}

@Test
fun deserialize() {
assertEquals(codec.decodeFromString<Failure>(jsonEmpty), failureEmpty)
assertEquals(codec.decodeFromString<Failure>(jsonFull), failureFull)
assertEquals(codec.decodeFromString<Failure>(jsonWithAttach), failureAttach)
}

private fun catch(block: () -> Nothing): FailureException {
try {
block()
} catch (e: FailureException) {
return e
}
}
}
14 changes: 14 additions & 0 deletions src/commonTest/kotlin/net/orandja/test/assertFailureEquals.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package net.orandja.test

import net.orandja.failure.Failure
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals

fun assertFailureEquals(first: Failure, second: Failure) {
assertEquals(first.id, second.id)
assertEquals(first.code, second.code)
assertEquals(first.description, second.description)
assertEquals(first.information, second.information)
assertEquals(first.cause, second.cause)
assertContentEquals(first.attached, second.attached?.asIterable())
}

0 comments on commit 41d5d48

Please sign in to comment.