Target
Target is a library for Functional Domain Modeling in Kotlin, inspired by arrow-kt.
Target aims to provide a set of tools across all Kotlin platforms to empower users to quickly write pure, functionally
validated domain models. For this, it includes a set of atomic components: ValueFailure, ValueObject,
and ValueValidator. These components can be used on their own, or in conjunction with the
included KSP annotation processor.
Getting Started
Value Failure
A ValueFailure is an interface representing a failure during value validation.
interface ValueFailure<T> {
val failedValue: T
}
Value Object
A ValueObject is an interface representing a validated value. By convention, value object implementations have a
private primary constructor, so that they are not instantiated outside a ValueValidator. A value object implementation
must declare a companion object implementing a value validator when used in conjunction with the annotation processor
library.
interface ValueObject<T> {
val value: T
}
Value Validator
A ValueValidator is an interface defining value validation functions. The primary validation function, of, takes an
input and returns either a ValueFailure or a ValueObject. By convention, a value validator implementation is an
abstract class, because the value object's private constructor is often passed to its primary constructor as a
reference.
interface ValueValidator<I, F : ValueFailure<I>, T : ValueObject<I>> {
fun of(input: I): Either<F, T>
}
Examples
The included StringInRegexValidator class is an example of a ValueValidator implementation.
abstract class StringInRegexValidator<T : ValueObject<String>>(private val ctor: (String) -> T) :
ValueValidator<String, GenericValueFailure<String>, T> {
protected abstract val regex: Regex
override : Either<GenericValueFailure<String>, T> {
(regex.matches(input)) {
Either.Right(ctor(input))
} {
Either.Left(GenericValueFailure(input))
}
}
}
Value object classes can be inlined on the JVM. This EmailAddress class is an example of such a ValueObject
implementation.
@JvmInline
value class EmailAddress private constructor(override val value: String) : ValueObject<String> {
companion object : EmailAddressValidator<EmailAddress>(::EmailAddress)
}
This value object can then be used to validate an email address like so:
suspend fun createUser(params: UserParamsDto) = either {
val emailAddress = EmailAddress.of(params.emailAddress).bind()
repositoryCreate(
UserParams(
emailAddress = emailAddress
)
).bind()
}
Annotation Processor
The Target annotation processor library makes it easy to create functionally validated models. It takes the fields
of a model data class and generates:
- A sealed set of failure classes.
- A validation function
Model.Companion.of() using said failure classes.
- A syntactic sugar function
Model.Companion.only() when the model contains one or more fields with an Option type.
Failure
The failure class is a sealed interface containing data classes for each value object property declared on the model
template, containing a single value, parent, with a type of the value object validator's failure type.
sealed interface ModelFieldFailure {
data class Property1(val parent: Property1Failure) : ModelFieldFailure
data class Property2(val parent: Property2Failure) : ModelFieldFailure
}
Validation Function
The validation function, named of, validates the model's fields similar to the behavior of a ValueValidator by
taking the raw value object field types and performing cumulative validation, calling each value object's validator
and returning either a non-empty list of model field failures or a model instance.
fun Model.Companion.of(): Either<Nel<ModelFieldFailure>, Model>
Optional Properties
It is also capable of validating optional value objects. This is useful when defining a model builder/update parameters
class representing updated model fields.
In addition to validating optional fields, the annotation processor will generate another function, named only, for
partial instantiation, applying a default of None to each of those fields. This is useful for only updating some
fields of a model without explicitly setting all others to None.
Here's a minimal example:
Nested Models
Nested models are a developing feature. A nested model field is defined just like any other field, with the type
of its model data class. Its definition in the validation function will be as follows:
@Validatable
data class Model(
val child: ChildModel
) {
companion object
}
fun Model.Companion.of(
child: Either<Nel<ChildModelFieldFailure>, ChildModel>
) {
TODO()
}
This delegates the validation of the model to the models own validation function. A failure for it will also be
generated for the parent model:
sealed interface ModelFieldFailure {
data class Child(val parent: Nel<ChildModelFieldFailure>) : ModelFieldFailure
}
Usage Example
Define a model data class:
Run a build and use the generated validation functions:
Gradle Setup
Note that these libraries are experimental, and their APIs are subject to change.
Target Core
dependencies {
implementation("io.target-kt:target-core:$targetVersion")
}
Target Core + Annotation Processor
plugins {
id("com.google.devtools.ksp") version kspVersion
}
dependencies {
implementation("io.target-kt:target-core:$targetVersion")
compileOnly("io.target-kt:target-annotation:$targetVersion")
ksp("io.target-kt:target-annotation-processor:$targetVersion")
}
See the KSP docs for additional configuration details.
Roadmap
- Add
Parseable annotation.
- Add
ValueObjectParser interface.
- Generate
Model.Companion.parse() function.
- Convert to compiler plugin and remove the need for
companion object stubs once a compiler plugin API is released.