commonMain.dev.fritz2.validation.validation.kt Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of core Show documentation
Show all versions of core Show documentation
Easily build reactive web-apps in Kotlin based on flows and coroutines
The newest version!
package dev.fritz2.validation
import dev.fritz2.core.Inspector
import dev.fritz2.core.inspectorOf
import kotlin.jvm.JvmInline
/**
* Encapsulates the logic for validating a given data-model with metadata.
*
* The validation logic itself is expressed by a function that must be passed as [validate] parameter.
* This function takes the actual model-data [D] as well as the metadata [T] in order to create a [List] of
* validation messages [M]. This value class simply wraps the provided [validate] function in order to make it
* invocable without any ceremony.
*
* It appears to be a good practice to put the implementation of the passed [validate] function right next to your data
* classes in the `commonMain` section of your Kotlin multiplatform project.
* This way you can write the validation logic once and use them on the *JS* and *JVM* side.
*
* For example:
* ```kotlin
* data class Person(val name: String, val birthday: LocalDate) {
* companion object {
* // define validator inside of its corresponding domain type
* val validate: Validator = validation { inspector, today ->
* inspector.map(Person.name()).let { nameInspector ->
* if(nameInspector.data.isBlank())
* add(SomeMessage(nameInspector.path, "Name must not be blank"))
* }
* inspector.map(Person.birthday()).let { birthdayInspector ->
* if(birthdayInspector.data > today)
* add(SomeMessage(birthdayInspector, path, "Birthday must not be in the future"))
* }
* }
* }
* }
* ```
*
* You can also compose validators:
* ```kotlin
* data class Person(val name: String, val birthday: LocalDate) {
* // take from example above!
* }
*
* data class User(val nickname: String, val person: Person) {
* data class UserMetaData(val nicknameRepo: NicknameRepository, val today: LocalDate)
*
* companion object {
* val validate: Validator = validation { inspector, meta ->
* inspector.map(User.nickname()).let { nicknameInspector ->
* if(meta.nicknameRepo.exists(nicknameInspector.data))
* add(SomeMessage(nicknameInspector.path, "Nickname is already in use"))
* }
* // use validator of `Person` type by just calling the validator and passing the mapped inspector
* // and of course the appropriate meta-data!
* addAll(Person.validate(inspector.map(User.person()), meta.today))
* }
* }
* }
* ```
*
* @param D data-model to validate
* @param T metadata which perhaps is needed in validation process
*/
@JvmInline
value class Validation(private inline val validate: (Inspector, T) -> List) {
operator fun invoke(inspector: Inspector, metadata: T): List = this.validate(inspector, metadata)
operator fun invoke(data: D, metadata: T): List = this.validate(inspectorOf(data), metadata)
}
/**
* Convenience execution function for [Validation] for the case, that the metadata is `Unit`.
* In those cases the metadata parameter can be omitted.
*
* @param inspector The [Inspector] of type [D] that should be used for the validation process
*/
operator fun Validation.invoke(inspector: Inspector): List = this(inspector, Unit)
/**
* Convenience execution function for [Validation] for the case, that the metadata is `Unit`.
* In those cases the metadata parameter can be omitted.
*
* @param data The data of type [D] that should be used for the validation process
*/
operator fun Validation.invoke(data: D): List = this(data, Unit)
/**
* Convenience function for creating a [Validation] instance accepting model- and metadata by working on a
* [MutableList] receiver and using an [Inspector] for getting the right [Inspector.path] from sub-models
* next to the [Inspector.data].
*/
fun validation(validate: MutableList.(Inspector, T) -> Unit): Validation =
Validation { data, metadata ->
buildList { validate(data, metadata) }
}
/**
* Convenience function for creating a [Validation] instance only accepting model-data by working on a
* [MutableList] receiver and using an [Inspector] for getting the right [Inspector.path] from sub-models
* next to the [Inspector.data].
*/
fun validation(validate: MutableList.(Inspector) -> Unit): Validation =
Validation { data, _ ->
buildList { validate(data) }
}
/**
* Minimal interface for a validation message that exposes the model path for matching relevant sub-model-data and
* probably relevant UI element representation for showing the message and getting information about the valid state
* after validation process.
*/
interface ValidationMessage {
/**
* Path inside your model derived from [Inspector.path]
*/
val path: String
/**
* Decides if the [ValidationMessage] is an error which is needed to determine if validation state is
* successful or not.
*
* It is intentional to explicitly define a message as an error to realize scenarios, where also pure information
* or warning messages could arise, that should *not* stop the process.
*
* If an application considers every message as error, just set this to `true`.
*/
val isError: Boolean
}
/**
* Returns *true* when the list contains no [ValidationMessage] which is marked with [ValidationMessage.isError].
*/
val List.valid: Boolean get() = none { it.isError }