implier icon indicating copy to clipboard operation
implier copied to clipboard

Data validation

Open y9vad9 opened this issue 4 years ago • 3 comments

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:
    var name: String = ...
    fun _checkName_(): Boolean = ...
    
    Transformed to →
    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:
    val name: Validate<String> = Validate(default = ...) { name -> require(name.length in 1..30) { "Provide a correct name" } }
    
    Transformed to →
    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

y9vad9 avatar Dec 02 '21 21:12 y9vad9

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 class and protected modifier (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))
   }
}

y9vad9 avatar Dec 02 '21 21:12 y9vad9

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.

y9vad9 avatar Aug 28 '22 22:08 y9vad9

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))
}

y9vad9 avatar Aug 28 '22 22:08 y9vad9