Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to create custom validators with the latest version? #56

Open
bargergo opened this issue May 23, 2020 · 5 comments
Open

How to create custom validators with the latest version? #56

bargergo opened this issue May 23, 2020 · 5 comments

Comments

@bargergo
Copy link
Contributor

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.

@bargergo
Copy link
Contributor Author

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

@Wicpar
Copy link
Collaborator

Wicpar commented May 23, 2020

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.

@bargergo
Copy link
Contributor Author

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

@Wicpar
Copy link
Collaborator

Wicpar commented May 24, 2020

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...

@bargergo
Copy link
Contributor Author

bargergo commented May 24, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants