First of all, if you want to work with abstract validatable objects, you need Validatable
interface:
interface Validatable {
fun validate()
}
You also need a class which represents a validated object:
data class Valid<out T : Validatable>(val obj: T) {
init {
obj.validate()
}
fun <U : Any> U?.mustBeValidated(): U = checkNotNull(this) {
"${obj::class.jvmName}.validate() successfully validated invalid object $obj"
}
}
Now you need Validatable.valid()
function which helps to create Valid
instances:
fun <T : Validatable> T.valid(): Valid<T> = Valid(this)
Here is how you can make your RequestModel
be Validatable
:
data class RequestModel(
val description: String?
) : Validatable {
override fun validate() {
requireNotNull(description) { "description must be non null" }
}
}
val Valid<RequestModel>.description get() = obj.description.mustBeValidated()
And here is how you can use it:
val validModel: Valid<RequestModel> = model.valid()
val notNullDescription: String = validModel.description
You can also make Valid
class inline. Since inline classes can't have init
blocks, init
logic is moved to the factory method. And since inline class primary constructor should be public
, the constructor is marked with @Experimental private annotation class ValidInternal
which prevents illegal constructor use:
@UseExperimental(ValidInternal::class)
fun <T : Validatable> T.valid(): Valid<T> {
validate()
return Valid(this)
}
@Experimental
private annotation class ValidInternal
inline class Valid<out T : Validatable> @ValidInternal constructor(
// Validatable is used here instead of T
// because inline class cannot have generic value parameter
private val _obj: Validatable
) {
@Suppress("UNCHECKED_CAST") // _obj is supposed to be T
val obj: T
get() = _obj as T
fun <U : Any> U?.mustBeValidated(): U = checkNotNull(this) {
"${obj::class}.validate() successfully validated invalid object $obj"
}
}