KResult

Maven Central Version Kotlin version License

Build Status Security Rating Coverage

An opinionated, functional Result type for Kotlin

KResult is a functional library that provides an opinionated and feature-rich extension / alternative to Kotlin's Result<T> type. While the core library is as lean and dependency-free as possible, seamless integrations with common frameworks are provided via integration packages or plugins. KResult is heavily inspired by Scala's Try[T] type, Rust's Result<T, E> and Arrow's Either<A, B>.

Features

Find full documentation on kresult.io.

Usage

KResult is hosted on Maven Central. Use your favorite build tool to add a dependency. Replace VERSION with the current version number:

Using Gradle Kotlin DSL:

dependencies {
implementation("io.kresult:kresult-core:VERSION")
}

Using Gradle Groovy DSL:

implementation group: 'io.kresult', name: 'kresult-core', version: 'VERSION'

Using Maven:

<dependency>
<groupId>io.kresult</groupId>
<artifactId>kresult-core</artifactId>
<version>VERSION</version>
</dependency>

Core Modules

Module Description Link
kresult-core Core module providing the functional KResult type as well as tooling and core functionality around it Docs / Maven
kresult-java Java interoperability for KResult and its transformations. Docs / Maven
kresult-problem RFC7807 compliant Problem Details JSON support for KResult. Docs / Maven

Integrations

Module Description Link
kresult-arrow Supports seamless integration with types of the Arrow functional programming library. Docs / Maven

Hello, World

import io.kresult.core.KResult
import io.kresult.core.filter

fun test() {
val greeting = KResult.Success("World")

val res: KResult<String, String> = greeting
// transform result, if successful
.map {
"""Hello, $it!"""
}
// transform to IllegalArgumentException on failure side, if value is blank
.filter(IllegalArgumentException("Greeting should not be blank")) {
it.isNotBlank()
}
// execute side-effect, if successful
.onSuccess {
print("log: $it is successful")
}
// transform failure from Exception to a string
.mapFailure {
"${it.javaClass}: ${it.localizedMessage}"
}

// fold result to string
res.fold(
{ error -> "Result failed with error: $error" },
{ value -> "Result was successful with value: $value" },
)
}

While strings are simple for showcasing, a more real world solution would have strongly typed result values. One common practice is to encode:

  • Success side value as a product type (data class in Kotlin)

  • Failure side value as a sum type / tagged union (enum or sealed class in Kotlin)

The following example demonstrates a very simplified case, where a greeting message is validated for correctness, and the resulting KResult<Failure, Greeting> is folded to an exemplary Response object again. In a more practical setup, that Response and the mechanics to produce it would of course be provided by a framework integration:

import io.kresult.core.*
import io.kotest.matchers.shouldBe

// Product to encode success
data class Greeting(val name: String)

// SUm to encode a failure
sealed class Failure(val msg: String) {

// Indicates that Failure was caused by invalid input
interface InputValidationProblem
data object BlankName : Failure("Name should not be blank"), InputValidationProblem
data object IllegalCharacters : Failure("Name contains illegal characters"), InputValidationProblem

data object Unknown : Failure("An unknown error occured")
}

// Product to encode a response
data class Response(val status: Int, val content: String)

fun test() {
val greeting: KResult<Failure, Greeting> = Greeting("World")
// Wraps any type into a KResult.Success
.asSuccess()
// Filter, transforming to failure if greeting name is blank
.filter(Failure.BlankName) {
it.name.isNotBlank()
}
.filter(Failure.IllegalCharacters) {
!it.name.contains("/")
}

val response: Response = greeting
// Transforms Success side to a Response
.map {
Response(status = 200, content = """Hello, ${it.name}!""")
}
// Transforms Failure side to a Response
.mapFailure { failure ->
when (failure) {
is Failure.InputValidationProblem ->
Response(status = 400, content = failure.msg)

else ->
Response(status = 500, content = failure.msg)
}
}
// When Success and Failure side are of the same type, we can merge them (long syntax for fold)
.merge()

response.status shouldBe 200
response.content shouldBe "Hello, World!"
}

License

Copyright 2024 kresult.io authors

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.

All modules:

Link copied to clipboard

The integration integration module provides functionality to integrate with the famous FP library for Kotlin, which was an inspiration for this project as well.

Link copied to clipboard

The core module contains all basic definitions and functionality, commonly used in KResult. Virtually all other modules depend on this either implicitly or explicitly.

Link copied to clipboard

Provides interoperability helpers to integrate with Java libraries.

Link copied to clipboard

Provides RFC7807 compliant Problem JSON support