y9vad9 / implier

Kotlin Symbol Processor library for creating Mutable, Immutable, Builders, DSL Builders from interfaces & abstract classes with properties.
17 stars 1 forks source link

Data validation #2

Open y9vad9 opened 2 years ago

y9vad9 commented 2 years ago

The most complex thing here is how to design validators. I see three ways:

Originally posted by @y9san9 in https://github.com/y9vad9/implier/issues/1#issuecomment-984492558

y9vad9 commented 2 years ago

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:

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 commented 2 years ago

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 commented 2 years ago

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