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.
A ValueFailure is an interface representing a failure during value validation.
interface ValueFailure<T> {
    val failedValue: T
}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
}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>
    // ...
}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 fun of(input: String): Either<GenericValueFailure<String>, T> {
        return if (regex.matches(input)) {
            Either.Right(ctor(input))
        } else {
            Either.Left(GenericValueFailure(input))
        }
    }
}Value object classes can be inlined on the JVM. This EmailAddress class is an example of such a ValueObject
implementation.
/**
 * A W3C HTML5 email address.
 */
@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()
    // ... validating other params ...
    repositoryCreate(
        UserParams(
            emailAddress = emailAddress
            // ... passing other validated params ...
        )
    ).bind()
}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 anOptiontype.
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
   // ...
}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(/* arguments with raw field types */): Either<Nel<ModelFieldFailure>, Model>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:
/**
 * Model builder used to update a model.
 */
@Validatable
data class ModelBuilder(
    val property1: Option<ModelProperty1>
) {
    companion object
}
/**
 * Validation function generated by the processor.
 */
fun ModelBuilder.Companion.of(
    property1: Option<RawModelProperty1>
): Either<Nel<ModelBuilderFieldFailure>, ModelBuilder> {
    TODO("...generated validation logic...")
}
/**
 * Syntactic function generated by the processor.
 */
fun ModelBuilder.Companion.only(
    property1: Option<ModelProperty1> = None
): ModelBuilder = ModelBuilder(property1)
/**
 * Function snippet of a usage example.
 */
fun updateModelProperty1(repository: ModelRepository, id: ModelId, property1: ModelProperty1) {
    repository.updateById(
        id = id,
        builder = ModelBuilder.only(
            property1 = property1.some()
            // ... all other builder properties will be set to None.
        )
    )
}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
}Define a model data class:
@Validatable
data class User(
    val id: PositiveInt,
    val firstName: FirstName,
    val lastName: LastName,
    val username: Username?,
    val emailAddress: EmailAddress,
    val phoneNumber: UserPhoneNumber?,
    val updated: Instant,
    val created: Instant
) {
    companion object
}
@Validatable
data class UserPhoneNumber(
    val userId: PositiveInt,
    val number: PhoneNumber,
    val validated: Boolean,
    val updated: Instant,
    val created: Instant
) {
    companion object
}
@Validatable
data class UserParams(
    val firstName: FirstName,
    val lastName: LastName,
    val username: Username?,
    val emailAddress: EmailAddress,
    val phoneNumber: UserPhoneNumberParams?
) {
    companion object
}
@Validatable
data class UserPhoneNumberParams(
    val number: PhoneNumber,
    val validated: Boolean
) {
    companion object
}
@Validatable
data class UserBuilder(
    val firstName: Option<FirstName>,
    val lastName: Option<LastName>,
    val username: Option<Username?>,
    val emailAddress: Option<EmailAddress>,
    val phoneNumber: Option<UserPhoneNumberBuilder?>
) {
    companion object
}
@Validatable
data class UserPhoneNumberBuilder(
   val number: Option<PhoneNumber>,
   val validated: Option<Boolean>
) {
   companion object
}Run a build and use the generated validation functions:
fun createUser() = either {
   repository.create(
      UserParams.of(
          firstName = "John",
          lastName = "Doe",
          username = "john.doe",
          emailAddress = "[email protected]",
          phoneNumber = UserPhoneNumberParams.of(
              number = "+11231231234",
              validated = false
          )
      ).bind()
   ).bind()
}
fun greetUser(user: User) {
    println("Hello, ${user.firstName.value}!")
    println("Your account was created on ${user.created}.")
}
fun textUser(user: User, message: SmsTextMessage) = either {
    ensureNotNull(user.phoneNumber) { NoPhoneNumber }.run {
        ensure(validated) { NotValidated }
        sendSms(number, message).bind()
    }
}
fun updateUser(id: PositiveInt) = repository.update(
    id,
    UserBuilder.only(
        username = null.some(),
        phoneNumber = UserPhoneNumberBuilder.only(
            validated = true.some()
        ).some()
    )
)Note that these libraries are experimental, and their APIs are subject to change.
dependencies {
    implementation("io.target-kt:target-core:$targetVersion")
}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.
- Add Parseableannotation.- Add ValueObjectParserinterface.
- Generate Model.Companion.parse()function.
 
- Add 
- Convert to compiler plugin and remove the need for companion objectstubs once a compiler plugin API is released.