konform-kt / konform

Portable validations for Kotlin
https://www.konform.io
MIT License
651 stars 39 forks source link

Add support for polymorphic list validation #59

Open inakov opened 1 year ago

inakov commented 1 year ago

Hello, I have one use-case that I cannot achieve with the library right now. I have a list of multiple common types and I want to have specific validation for each type. For example:

sealed interface Event {
    val id: ObjectId
    val start: Instant
    val end: Instant
    ... some other common fileds
}

data class ConcertEvent(
     val band: String,
     ... other fields
): Event

data class TheaterEvent(
     val stage: String,
     ... other fields
): Event

data class Subscribe(
    val events: List<Event>,
    ... other fields
);

Currently, if I want to add validation for Subscribe.events I can do this only by validating Event fields with

Subscribe.events onEach {
     here we cannot have validation for `TheaterEvent` or `ConcertEvent` fields only for `Event`
}

I think is going to be very useful if we have something like this

Subscribe.events onEach<ConcertEvent> {
    ConcertEvent::band required {}
}

Subscribe.events onEach<TheaterEvent> {
    TheaterEvent::stage required {}
   ... other validations
}

What do you think? Is it possible to achieve similar result? Do you think there would be a better approach?

PS. Konform is great! I love it.

dhoepelman commented 3 months ago

This would be good to have support for. It's probably relatively easy to do, I think some thing(s) are not marked as covariant when they should be.

An existing workaround for this is:

@Supress("UNCHECKED_CAST")
fun <T: Event> eventValidation = Validation<Event> {
  Event::stage required {}
} as Validation<T>

val theaterEventValidation = eventValidation<TheaterEvent>
koenreiniers commented 2 months ago

Ran into a similar issue today. This is is how I solved it:

class Canvas(val shapes: List<Shape>)
sealed interface Shape
class Circle(val radius: Int): Shape
class Square(val sideLength: Int): Shape

class ShapeValidation : Validation<Shape> {
    private val circleValidation = Validation {
        Circle::radius {
            minimum(0)
        }
    }

    private val squareValidation = Validation {
        Square::sideLength {
            minimum(0)
        }
    }

    override fun validate(value: Shape): ValidationResult<Shape> {
        return when (value) {
            is Circle -> circleValidation(value)
            is Square -> squareValidation(value)
        }
    }
}

val canvasValidation = Validation {
    Canvas::shapes onEach {
        run(ShapeValidation())
    }
}