Skip to content

ustitc/krefty

Repository files navigation

Krefty

CI Licence Maven Central

Krefty is a Kotlin library that empowers the creation of domain-specific types while addressing the Primitive Obsession anti-pattern. It provides a robust framework for constructing types inspired by the Refinement Type Theory, where types are composed of a predicate and a value that satisfies it.

Krefty is particularly useful for Domain-Driven Design (DDD) users, where refined types can be viewed as a viable alternative to Value Objects. Inspired by implementations in Haskell and Scala.

Also check out Arrow-Exact and Values4k which solve the same problem.

Getting started 🚀

Add Krefty to your dependencies:

implementation("dev.ustits.krefty:krefty-core:<latest_version>")

For snapshot versions

repositories {
    maven("https://s01.oss.sonatype.org/content/repositories/snapshots")
}

implementation("dev.ustits.krefty:krefty-core:<latest_version>-SNAPSHOT")

Usage

Refinery 🏭

Let's consider an example where we need a type for describing names. We must ensure that the name is not empty and that we have a specific type for it:

@JvmInline
value class Name private constructor(private val value: String) {

    companion object : Refinery<String, Name>() {
        override fun Refinement<String>.refine() = filter { it.isNotBlank() }.map { Name(it) }
    }
}

We define a Name class that holds a String value. Refinery serves as a medium to fine-tune the conversion from String to Name. This is done by implementing refine() function, which applies filters to check if the string is non-empty. If the string satisfies this condition, it is then transformed into a Name instance.

You now have the ability to generate Name instances, for example by using the fromOrThrow method:

val grog = Name.fromOrThrow("Grog") // Name instance "Grog"
val void = Name.fromOrThrow("")     // throws RefinementException

For a simplified version of from, you can apply specific Refinery implementations:

  • NullRefinery
  • ThrowingRefinery
  • ResultRefinery

For instance, with ResultRefinery, the usage would look like this:

companion object : ResultRefinery<String, Name>()

Name.from("Scanlan")  // Result.success

Refinement 🛢️

Refinement is a core concept in Krefty. You can think of it as a container for a value and a predicate. If the value matches the predicate, it holds that value, otherwise, it holds an error.

Refinement provides familiar operations, like map and filter, to validate and transform the refined type:

refinement
    .filter { it.isNotBlank() }
    .map { NotBlankString(it) }
    .flatMap { refine(it, this::isName) }

Refinery itself can also be used as a transformation:

refinement
    .filter(NotBlankString)
    .flatMap(Name)

You can also use Refinement separately from Refinery, for example by using refine function:

val name = refine("Krefty") { it.isNotBlank() } 
name.getOrThrow()       // "Krefty"
name.isRefined()        // true

val version = refine("") { it.isNotBlank() }
version.getOrThrow()    // throws RefinementException
version.isRefined()     // false

For refinements that involve side effects, the suspendRefine function can be used:

suspendRefine("94926946-2e51-4b14-a9bd-2ce9ad02b29b") {
    service.existsById(it)
}

class Service {
    suspend fun existsById(id: String): Boolean
}

Arrow

Krefty can be used with Either type from Arrow. In order to use it add krefty-arrow to your dependencies:

implementation("dev.ustits.krefty:krefty-arrow:<latest_version>")

Then you can use EitherRefinery to get results as an Either:

Name.from("Keyleth") // Either<RefinementError, Name>