Data validation
The most complex thing here is how to design validators. I see three ways:
- (Not convenient for me, but still the way) design it with notations and annotations:
// object here is notation, we assuming that user will // create object here, but not class. Anyway this will // be checked at compile time object NameValidator : Validator<String> { override fun validate(value: String): Boolean = ... } @Validate(NameValidator::class) val name: String = "" - (Not convenient for me, but still the way) design it with function name notations:
Transformed to →var name: String = ... fun _checkName_(): Boolean = ...private var _name: String var name: String get() = _name set() = if(/* code from _checkName_() */) ... else ... - (Now I'm looking forward this) Wrap it with a type:
Transformed to →val name: Validate<String> = Validate(default = ...) { name -> require(name.length in 1..30) { "Provide a correct name" } }var _name: String = default var name get() = _name set() = if(/* code from name.validate() */) ... else ...
Originally posted by @y9san9 in https://github.com/y9vad9/implier/issues/1#issuecomment-984492558
I don't see any variants for wrapping concretely value (with saving interface implementation for mutable & immutable realizations) via ksp so I suggest next variant:
@ImmutableImpl
interface Foo {
val value: String
@Validator(propertyName = "value")
val validator: Validator<String> get() = StringLengthValidator(1..99)
}
Minuses I see:
- validator is exposing to public (we can use internal modifier, but it isn't a case for every situation)
Possible solutions: It can be fixed with
abstract classandprotectedmodifier (requires #3)
Also, with same idea we can make companion object with next view:
@ImmutableImpl
interface Foo {
val value: String
@ValidatorsStorage
private companion object Validation {
val value: Validatable<String> = Validatable(property = Foo::value, validator = StringLengthValidator(0..99))
}
}
finally, I propose next variant:
/**
* Validator contract:
* - Validator always should be object.
*/
interface Validator<T> {
/**
* Checks [value] for correctness.
* If return value is `false` — implier will throw an exception.
*/
fun validate(value: T): Boolean
}
annotation class Validates<T>(val validator: KClass<Validator<T>>)
object DigitStringValidator : Validator<String> {
override fun validate(value: String): Boolean {
return value.all { it.isDigit() }
}
}
@ImmutableImpl
interface Entity {
@Validates(DigitStringValidator::class)
val value: String
}
Minuse I see is impossibility to provide some additional parameters to validator. But I think it can be avoided in next way:
abstract class IntValueValidator(val min: Int, val max: Int) : Validator<Int> {
override fun validate(value: Int): Boolean {
return value >= min && value =< max
}
companion object Month : IntValueValidator(1, 12)
companion object Hour24 : IntValueValidator(0, 23)
// etc
}
However it still not a case for some situations, but I don't see any other possible solutions.
Anyway, we can use init {} block in abstract classes to validate the information.
Also, we can provide annotation that will provide safe way to institiate an object:
@Immutable
@SafeFactoryImpl
interface Entity {
@Validates(EmailValidator::class)
val email: String
@Validates(StringLengthValidator.FirstName::class)
val firstName: String
}
// generates
sealed interface EntityCreationResult {
object EmailIsInvalid : EntityCreationResult
object FirstNameIsInvalid : EntityCreationResult
class Success(val value: Entity) : EntityCreationResult
}
fun Entity(email: String, firstName: String): EntityCreationResult {
if(!EmailValidator.validate(email))
return EntityCreationResult.EmailIsInvalid
if(!StringLengthValidator.FirstName.validate(firstName))
return EntityLengthValidator.FirstNameIsInvalid
return EmailCreationResult.Success(ImmutableEntity(email, firstName))
}