Reputation: 5493
I have a class with some nullable properties
data class RequestModel(
val description: String?
)
and a validation function
fun validate(model: RequestModel): RequestModel{
if(model.description == null) throw IllegalArgumentException("description must be non null")
return model
}
After this validation step, I need a way to indicate non-nullability of description
property.
One solution is to create a new data class which has non null propertis data class RequestModel(val description: String)
.
But I'm looking for a generic way to avoid creating new classes per use case.
Ideal generic solution:
fun validate(model: RequestModel): NoNullableField<RequestModel>
How can I remove nullability from properties of a class with nullable properties in a generic way? Is it usefull to use some kind of kotlin compiler contract?
Upvotes: 2
Views: 1773
Reputation: 2492
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"
}
}
Upvotes: 1
Reputation: 2492
You can use Kotlin reflection to get all properties and check if they are not null:
inline fun <reified T : Any> T.requireNoNullableProperties() = NoNullableProperties(this, T::class)
class NoNullableProperties<out T : Any>(val obj: T, clazz: KClass<T>) {
init {
clazz.memberProperties.forEach { prop ->
if (prop.returnType.isMarkedNullable) {
prop.isAccessible = true
requireNotNull(prop.get(obj)) {
"${prop.name} must be not null, obj - [$obj]"
}
}
}
}
operator fun <R> get(property: KProperty1<in T, R?>): R = requireNotNull(property.get(obj)) {
"Extension and mutable properties can't be validated, property - [$property], obj - [$obj]"
}
}
Use case:
val validated = model.requireNoNullableProperties()
val description: String = validated[RequestModel::description]
Also, you can extract validated[RequestModel::description]
to an extension property of NoNullableProperties<RequestModel>
:
val ValidRequestModel.description get() = get(RequestModel::description)
Where ValidRequestModel
is:
typealias ValidRequestModel = NoNullableProperties<RequestModel>
Use case:
val validated = model.requireNoNullableProperties()
val description: String = validated.description
Upvotes: 1