Open bargergo opened 4 years ago
Ok, now I've found some example code from the built-in validators/transformers, but I still don't know how could I write a custom validator that validates if the length of a string equals to a number.
Annotation
package com.papsign.ktor.openapigen.validation.string.lowercase
import com.papsign.ktor.openapigen.validation.ValidatorAnnotation
@Target(AnnotationTarget.PROPERTY, AnnotationTarget.TYPE)
@ValidatorAnnotation(LowerCaseValidator::class)
annotation class LowerCase
Validator
package com.papsign.ktor.openapigen.validation.string.lowercase
import com.papsign.ktor.openapigen.getKType
import com.papsign.ktor.openapigen.validation.Validator
import com.papsign.ktor.openapigen.validation.util.SingleTypeValidator
object LowerCaseValidator : SingleTypeValidator<LowerCase>(getKType<String>(), { LowerCaseValidator }), Validator {
override fun <T> validate(subject: T?): T? {
@Suppress("UNCHECKED_CAST")
return (subject as String?)?.toLowerCase() as T?
}
}
Yes it changed quite a bit to get rid of the registration process, now you can just provide the annotation and the annotation itself provides the handler. Take a look at https://github.com/papsign/Ktor-OpenAPI-Generator/blob/master/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/NumberConstraintProcessor.kt
Just ignore the abstract part, implement the SchemaProcessor and Validator builder. If the validation fails an exception should be thrown.
Then you create an annotation like this:
@Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY)
@SchemaProcessorAnnotation(MinProcessor::class)
@ValidatorAnnotation(MinProcessor::class)
annotation class Min(val value: Long)
I will have to write a wiki page for it indeed.
Thanks for your help! I've managed to implement the custom validator I wanted. I'm sharing my code, maybe it will be helpful for others.
I've wrote a LengthConstraintProcessor class based on the NumberConstraintProcessor class
abstract class LengthConstraintProcessor<A: Annotation>(): SchemaProcessor<A>, ValidatorBuilder<A> {
val types = listOf(getKType<String>().withNullability(true), getKType<String>().withNullability(false))
abstract fun process(model: SchemaModel<*>, annotation: A): SchemaModel<*>
abstract fun getConstraint(annotation: A): LengthConstraint
private class LengthConstraintValidator(private val constraint: LengthConstraint): Validator {
override fun <T> validate(subject: T?): T? {
if (subject is String?) {
val value = subject?.length ?: 0
if (constraint.min != null) {
if (value < constraint.min) throw LengthConstraintViolation(value, constraint)
}
if (constraint.max != null) {
if (value > constraint.max) throw LengthConstraintViolation(value, constraint)
}
} else {
throw NotAStringViolation(subject)
}
return subject
}
}
override fun build(type: KType, annotation: A): Validator {
return if (types.contains(type)) {
LengthConstraintValidator(getConstraint(annotation))
} else {
error("${annotation::class} can only be used on types: $types")
}
}
override fun process(model: SchemaModel<*>, type: KType, annotation: A): SchemaModel<*> {
return if (types.contains(type)) {
process(model, annotation)
} else {
model
}
}
}
data class LengthConstraint(val min: Int? = null, val max: Int? = null, val errorMessage: String? = null)
open class ConstraintViolation(message: String, cause: Throwable? = null): Exception(message, cause)
class LengthConstraintViolation(val actual: Number?, val constraint: LengthConstraint): ConstraintViolation(constraint.errorMessage ?: "Constraint violation: the length of the string should be ${
{
val min = "${constraint.min}"
val max = "${constraint.max}"
when {
constraint.min != null && constraint.max != null -> "between $min and $max"
constraint.min != null -> "at least $min"
constraint.max != null -> "at most $max"
else -> "anything"
}
}()
}, but it is $actual")
class NotAStringViolation(val value: Any?): ConstraintViolation("Constraint violation: $value is not a string")
Annotation
@Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY)
@SchemaProcessorAnnotation(ExactLengthProcessor::class)
@ValidatorAnnotation(ExactLengthProcessor::class)
annotation class ExactLength(val value: Int, val message: String = "")
Processor class
object ExactLengthProcessor : LengthConstraintProcessor<ExactLength>() {
override fun process(model: SchemaModel<*>, annotation: ExactLength): SchemaModel<*> {
// There is no string schema and couldn't create one, because SchemaModel is a sealed class
return model
}
override fun getConstraint(annotation: ExactLength): LengthConstraint {
val errorMessage = if (annotation.message.isNotEmpty()) annotation.message else null
return LengthConstraint(min = annotation.value, max = annotation.value, errorMessage = errorMessage)
}
}
Nice :) Feel free to open a pull request if you wish to contribute the annotations and add the missing properties in the model. Maybe i should change the model to a delegated hashmap, constantly changing the model is getting tedious...
Ok, I've created a pull request. I couldn't test it, yet, because I have problems with adding the modified gradle project as dependency to a project correctly.
Edit: I have managed to import the modified project somehow. The validation works, but the new constraints are missing from the schema. I think the Schema builders can't handle the new SchemaModelString class correctly.
I've created custom validators with the 0.1-beta.2 version and I wanted to update to the latest version (0.2-beta.5). The validation system changed a lot and I can't figure it out how to write a custom validator. I've only found example on custom validators for the old version.