diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/common/ConstraintViolation.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/common/ConstraintViolation.kt new file mode 100644 index 000000000..ac6d6b854 --- /dev/null +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/common/ConstraintViolation.kt @@ -0,0 +1,6 @@ +package com.papsign.ktor.openapigen.annotations.type.common + +import java.lang.Exception + +abstract class ConstraintViolation(defaultMessage: String, message: String = "", cause: Throwable? = null) + : Exception(if (message.isEmpty()) defaultMessage else message, cause) \ No newline at end of file diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/NumberConstraintProcessor.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/NumberConstraintProcessor.kt index 354173711..955102bc8 100644 --- a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/NumberConstraintProcessor.kt +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/NumberConstraintProcessor.kt @@ -1,11 +1,11 @@ package com.papsign.ktor.openapigen.annotations.type.number +import com.papsign.ktor.openapigen.annotations.type.common.ConstraintViolation import com.papsign.ktor.openapigen.classLogger import com.papsign.ktor.openapigen.model.schema.SchemaModel import com.papsign.ktor.openapigen.schema.processor.SchemaProcessor import com.papsign.ktor.openapigen.validation.Validator import com.papsign.ktor.openapigen.validation.ValidatorBuilder -import java.lang.Exception import java.math.BigDecimal import kotlin.reflect.KType import kotlin.reflect.full.withNullability @@ -59,11 +59,9 @@ abstract class NumberConstraintProcessor(allowedTypes: Iterable "anything" } }() -}") +}", constraint.errorMessage) -class NotANumberViolationViolation(val value: Any?): ConstraintVialoation("Constraint violation: $value is not a number") +class NotANumberViolationViolation(val value: Any?): ConstraintViolation("Constraint violation: $value is not a number") diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/clamp/FClamp.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/clamp/FClamp.kt index 7a1d240e1..67bba6664 100644 --- a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/clamp/FClamp.kt +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/clamp/FClamp.kt @@ -6,6 +6,6 @@ import com.papsign.ktor.openapigen.validation.ValidatorAnnotation @Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY) @SchemaProcessorAnnotation(FClampProcessor::class) @ValidatorAnnotation(FClampProcessor::class) -annotation class FClamp(val min: Double, val max: Double) +annotation class FClamp(val min: Double, val max: Double, val errorMessage: String = "") diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/clamp/FClampProcessor.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/clamp/FClampProcessor.kt index 0c4b537a1..437eb850b 100644 --- a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/clamp/FClampProcessor.kt +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/clamp/FClampProcessor.kt @@ -18,6 +18,6 @@ object FClampProcessor : FloatingNumberConstraintProcessor() { } override fun getConstraint(annotation: FClamp): NumberConstraint { - return NumberConstraint(BigDecimal(annotation.min), BigDecimal(annotation.max)) + return NumberConstraint(BigDecimal(annotation.min), BigDecimal(annotation.max), errorMessage = annotation.errorMessage) } } diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/max/FMax.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/max/FMax.kt index 56b12cbeb..5c32a62ee 100644 --- a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/max/FMax.kt +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/max/FMax.kt @@ -6,4 +6,4 @@ import com.papsign.ktor.openapigen.validation.ValidatorAnnotation @Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY) @SchemaProcessorAnnotation(FMaxProcessor::class) @ValidatorAnnotation(FMaxProcessor::class) -annotation class FMax(val value: Double) +annotation class FMax(val value: Double, val errorMessage: String = "") diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/max/FMaxProcessor.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/max/FMaxProcessor.kt index 44fdba0fe..6a2bbcb9f 100644 --- a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/max/FMaxProcessor.kt +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/max/FMaxProcessor.kt @@ -16,6 +16,6 @@ object FMaxProcessor: FloatingNumberConstraintProcessor() { } } override fun getConstraint(annotation: FMax): NumberConstraint { - return NumberConstraint(max= BigDecimal(annotation.value)) + return NumberConstraint(max= BigDecimal(annotation.value), errorMessage = annotation.errorMessage) } } diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/min/FMin.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/min/FMin.kt index bbef42c80..dc5f39deb 100644 --- a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/min/FMin.kt +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/min/FMin.kt @@ -6,4 +6,4 @@ import com.papsign.ktor.openapigen.validation.ValidatorAnnotation @Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY) @SchemaProcessorAnnotation(FMinProcessor::class) @ValidatorAnnotation(FMinProcessor::class) -annotation class FMin(val value: Double) +annotation class FMin(val value: Double, val errorMessage: String = "") diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/min/FMinProcessor.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/min/FMinProcessor.kt index ab7d2cb87..f1417c86e 100644 --- a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/min/FMinProcessor.kt +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/floating/min/FMinProcessor.kt @@ -17,6 +17,6 @@ object FMinProcessor: FloatingNumberConstraintProcessor() { } override fun getConstraint(annotation: FMin): NumberConstraint { - return NumberConstraint(min = BigDecimal(annotation.value)) + return NumberConstraint(min = BigDecimal(annotation.value), errorMessage = annotation.errorMessage) } } diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/clamp/Clamp.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/clamp/Clamp.kt index 88d358b70..dc6d0bc20 100644 --- a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/clamp/Clamp.kt +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/clamp/Clamp.kt @@ -6,4 +6,4 @@ import com.papsign.ktor.openapigen.validation.ValidatorAnnotation @Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY) @SchemaProcessorAnnotation(ClampProcessor::class) @ValidatorAnnotation(ClampProcessor::class) -annotation class Clamp(val min: Long, val max: Long) +annotation class Clamp(val min: Long, val max: Long, val errorMessage: String = "") diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/clamp/ClampProcessor.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/clamp/ClampProcessor.kt index d88d14f04..3da02d98a 100644 --- a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/clamp/ClampProcessor.kt +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/clamp/ClampProcessor.kt @@ -18,6 +18,6 @@ object ClampProcessor: IntegerNumberConstraintProcessor() { } override fun getConstraint(annotation: Clamp): NumberConstraint { - return NumberConstraint(BigDecimal(annotation.min), BigDecimal(annotation.max)) + return NumberConstraint(BigDecimal(annotation.min), BigDecimal(annotation.max), errorMessage = annotation.errorMessage) } } diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/max/Max.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/max/Max.kt index 822f415a3..75600e63c 100644 --- a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/max/Max.kt +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/max/Max.kt @@ -6,4 +6,4 @@ import com.papsign.ktor.openapigen.validation.ValidatorAnnotation @Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY) @SchemaProcessorAnnotation(MaxProcessor::class) @ValidatorAnnotation(MaxProcessor::class) -annotation class Max(val value: Long) +annotation class Max(val value: Long, val errorMessage: String = "") diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/max/MaxProcessor.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/max/MaxProcessor.kt index 360b6a5c0..87245a256 100644 --- a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/max/MaxProcessor.kt +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/max/MaxProcessor.kt @@ -16,6 +16,6 @@ object MaxProcessor: IntegerNumberConstraintProcessor() { } } override fun getConstraint(annotation: Max): NumberConstraint { - return NumberConstraint(max= BigDecimal(annotation.value)) + return NumberConstraint(max= BigDecimal(annotation.value), errorMessage = annotation.errorMessage) } } diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/min/Min.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/min/Min.kt index 9c75d8de4..13a2a37ed 100644 --- a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/min/Min.kt +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/min/Min.kt @@ -6,4 +6,4 @@ import com.papsign.ktor.openapigen.validation.ValidatorAnnotation @Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY) @SchemaProcessorAnnotation(MinProcessor::class) @ValidatorAnnotation(MinProcessor::class) -annotation class Min(val value: Long) +annotation class Min(val value: Long, val errorMessage: String = "") diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/min/MinProcessor.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/min/MinProcessor.kt index 1a595396e..4c253b52e 100644 --- a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/min/MinProcessor.kt +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/number/integer/min/MinProcessor.kt @@ -17,6 +17,6 @@ object MinProcessor: IntegerNumberConstraintProcessor() { } override fun getConstraint(annotation: Min): NumberConstraint { - return NumberConstraint(min = BigDecimal(annotation.value)) + return NumberConstraint(min = BigDecimal(annotation.value), errorMessage = annotation.errorMessage) } } diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/NotAStringViolation.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/NotAStringViolation.kt new file mode 100644 index 000000000..daae588b6 --- /dev/null +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/NotAStringViolation.kt @@ -0,0 +1,5 @@ +package com.papsign.ktor.openapigen.annotations.type.string + +import com.papsign.ktor.openapigen.annotations.type.common.ConstraintViolation + +class NotAStringViolation(val value: Any?): ConstraintViolation("Constraint violation: $value is not a string") \ No newline at end of file diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/Length.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/Length.kt new file mode 100644 index 000000000..5ecba5a68 --- /dev/null +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/Length.kt @@ -0,0 +1,9 @@ +package com.papsign.ktor.openapigen.annotations.type.string.length + +import com.papsign.ktor.openapigen.schema.processor.SchemaProcessorAnnotation +import com.papsign.ktor.openapigen.validation.ValidatorAnnotation + +@Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY) +@SchemaProcessorAnnotation(LengthProcessor::class) +@ValidatorAnnotation(LengthProcessor::class) +annotation class Length(val min: Int, val max: Int, val errorMessage: String = "") \ No newline at end of file diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/LengthConstraintProcessor.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/LengthConstraintProcessor.kt new file mode 100644 index 000000000..81c0040fb --- /dev/null +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/LengthConstraintProcessor.kt @@ -0,0 +1,72 @@ +package com.papsign.ktor.openapigen.annotations.type.string.length + +import com.papsign.ktor.openapigen.annotations.type.common.ConstraintViolation +import com.papsign.ktor.openapigen.annotations.type.string.NotAStringViolation +import com.papsign.ktor.openapigen.classLogger +import com.papsign.ktor.openapigen.getKType +import com.papsign.ktor.openapigen.model.schema.SchemaModel +import com.papsign.ktor.openapigen.schema.processor.SchemaProcessor +import com.papsign.ktor.openapigen.validation.Validator +import com.papsign.ktor.openapigen.validation.ValidatorBuilder +import kotlin.reflect.KType +import kotlin.reflect.full.withNullability + +abstract class LengthConstraintProcessor(): SchemaProcessor, ValidatorBuilder { + + private val log = classLogger() + + val types = listOf(getKType().withNullability(true), getKType().withNullability(false)) + + abstract fun process(model: SchemaModel.SchemaModelLitteral<*>, annotation: A): SchemaModel.SchemaModelLitteral<*> + + abstract fun getConstraint(annotation: A): LengthConstraint + + private class LengthConstraintValidator(private val constraint: LengthConstraint): Validator { + override fun 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 (model is SchemaModel.SchemaModelLitteral<*> && types.contains(type)) { + process(model, annotation) + } else { + log.warn("${annotation::class} can only be used on types: $types") + model + } + } +} + +data class LengthConstraint(val min: Int? = null, val max: Int? = null, val errorMessage: String) + +class LengthConstraintViolation(val actual: Number?, val constraint: LengthConstraint): ConstraintViolation("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", constraint.errorMessage) \ No newline at end of file diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/LengthProcessor.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/LengthProcessor.kt new file mode 100644 index 000000000..7ab93c20f --- /dev/null +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/LengthProcessor.kt @@ -0,0 +1,17 @@ +package com.papsign.ktor.openapigen.annotations.type.string.length + +import com.papsign.ktor.openapigen.model.schema.SchemaModel + +object LengthProcessor : LengthConstraintProcessor() { + override fun process(model: SchemaModel.SchemaModelLitteral<*>, annotation: Length): SchemaModel.SchemaModelLitteral<*> { + @Suppress("UNCHECKED_CAST") + return (model as SchemaModel.SchemaModelLitteral).apply { + maxLength = annotation.max + minLength = annotation.min + } + } + + override fun getConstraint(annotation: Length): LengthConstraint { + return LengthConstraint(min = annotation.min, max = annotation.max, errorMessage = annotation.errorMessage) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/MaxLength.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/MaxLength.kt new file mode 100644 index 000000000..e689a4b8b --- /dev/null +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/MaxLength.kt @@ -0,0 +1,9 @@ +package com.papsign.ktor.openapigen.annotations.type.string.length + +import com.papsign.ktor.openapigen.schema.processor.SchemaProcessorAnnotation +import com.papsign.ktor.openapigen.validation.ValidatorAnnotation + +@Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY) +@SchemaProcessorAnnotation(MaxLengthProcessor::class) +@ValidatorAnnotation(MaxLengthProcessor::class) +annotation class MaxLength(val value: Int, val errorMessage: String = "") \ No newline at end of file diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/MaxLengthProcessor.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/MaxLengthProcessor.kt new file mode 100644 index 000000000..ea072d26d --- /dev/null +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/MaxLengthProcessor.kt @@ -0,0 +1,16 @@ +package com.papsign.ktor.openapigen.annotations.type.string.length + +import com.papsign.ktor.openapigen.model.schema.SchemaModel + +object MaxLengthProcessor : LengthConstraintProcessor() { + override fun process(model: SchemaModel.SchemaModelLitteral<*>, annotation: MaxLength): SchemaModel.SchemaModelLitteral<*> { + @Suppress("UNCHECKED_CAST") + return (model as SchemaModel.SchemaModelLitteral).apply { + maxLength = annotation.value + } + } + + override fun getConstraint(annotation: MaxLength): LengthConstraint { + return LengthConstraint(max = annotation.value, errorMessage = annotation.errorMessage) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/MinLength.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/MinLength.kt new file mode 100644 index 000000000..0c5f82f1e --- /dev/null +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/MinLength.kt @@ -0,0 +1,9 @@ +package com.papsign.ktor.openapigen.annotations.type.string.length + +import com.papsign.ktor.openapigen.schema.processor.SchemaProcessorAnnotation +import com.papsign.ktor.openapigen.validation.ValidatorAnnotation + +@Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY) +@SchemaProcessorAnnotation(MinLengthProcessor::class) +@ValidatorAnnotation(MinLengthProcessor::class) +annotation class MinLength(val value: Int, val errorMessage: String = "") \ No newline at end of file diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/MinLengthProcessor.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/MinLengthProcessor.kt new file mode 100644 index 000000000..ac4c27c2e --- /dev/null +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/length/MinLengthProcessor.kt @@ -0,0 +1,16 @@ +package com.papsign.ktor.openapigen.annotations.type.string.length + +import com.papsign.ktor.openapigen.model.schema.SchemaModel + +object MinLengthProcessor : LengthConstraintProcessor() { + override fun process(model: SchemaModel.SchemaModelLitteral<*>, annotation: MinLength): SchemaModel.SchemaModelLitteral<*> { + @Suppress("UNCHECKED_CAST") + return (model as SchemaModel.SchemaModelLitteral).apply { + minLength = annotation.value + } + } + + override fun getConstraint(annotation: MinLength): LengthConstraint { + return LengthConstraint(min = annotation.value, errorMessage = annotation.errorMessage) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/pattern/RegularExpression.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/pattern/RegularExpression.kt new file mode 100644 index 000000000..06e054177 --- /dev/null +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/pattern/RegularExpression.kt @@ -0,0 +1,9 @@ +package com.papsign.ktor.openapigen.annotations.type.string.pattern + +import com.papsign.ktor.openapigen.schema.processor.SchemaProcessorAnnotation +import com.papsign.ktor.openapigen.validation.ValidatorAnnotation + +@Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY) +@SchemaProcessorAnnotation(RegularExpressionProcessor::class) +@ValidatorAnnotation(RegularExpressionProcessor::class) +annotation class RegularExpression(@org.intellij.lang.annotations.Language("RegExp") val pattern: String, val errorMessage: String = "") diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/pattern/RegularExpressionConstraintProcessor.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/pattern/RegularExpressionConstraintProcessor.kt new file mode 100644 index 000000000..353a99f31 --- /dev/null +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/pattern/RegularExpressionConstraintProcessor.kt @@ -0,0 +1,58 @@ +package com.papsign.ktor.openapigen.annotations.type.string.pattern + +import com.papsign.ktor.openapigen.annotations.type.common.ConstraintViolation +import com.papsign.ktor.openapigen.annotations.type.string.NotAStringViolation +import com.papsign.ktor.openapigen.classLogger +import com.papsign.ktor.openapigen.getKType +import com.papsign.ktor.openapigen.model.schema.SchemaModel +import com.papsign.ktor.openapigen.schema.processor.SchemaProcessor +import com.papsign.ktor.openapigen.validation.Validator +import com.papsign.ktor.openapigen.validation.ValidatorBuilder +import kotlin.reflect.KType +import kotlin.reflect.full.withNullability + +abstract class RegularExpressionConstraintProcessor(): SchemaProcessor, ValidatorBuilder { + + private val log = classLogger() + + val types = listOf(getKType().withNullability(true), getKType().withNullability(false)) + + abstract fun process(model: SchemaModel.SchemaModelLitteral<*>, annotation: A): SchemaModel.SchemaModelLitteral<*> + + abstract fun getConstraint(annotation: A): RegularExpressionConstraint + + private class RegularExpressionConstraintValidator(private val constraint: RegularExpressionConstraint): Validator { + override fun validate(subject: T?): T? { + if (subject is String?) { + if (subject == null || !constraint.pattern.toRegex().containsMatchIn(subject)) { + throw RegularExpressionConstraintViolation(subject, constraint) + } + } else { + throw NotAStringViolation(subject) + } + return subject + } + } + + override fun build(type: KType, annotation: A): Validator { + return if (types.contains(type)) { + RegularExpressionConstraintValidator(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 (model is SchemaModel.SchemaModelLitteral<*> && types.contains(type)) { + process(model, annotation) + } else { + log.warn("${annotation::class} can only be used on types: $types") + model + } + } +} + +data class RegularExpressionConstraint(val pattern: String, val errorMessage: String) + +class RegularExpressionConstraintViolation(val actual: String?, val constraint: RegularExpressionConstraint): ConstraintViolation("Constraint violation: the string " + +"'$actual' does not match the regular expression ${constraint.pattern}", constraint.errorMessage) \ No newline at end of file diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/pattern/RegularExpressionProcessor.kt b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/pattern/RegularExpressionProcessor.kt new file mode 100644 index 000000000..ac4b52afa --- /dev/null +++ b/src/main/kotlin/com/papsign/ktor/openapigen/annotations/type/string/pattern/RegularExpressionProcessor.kt @@ -0,0 +1,16 @@ +package com.papsign.ktor.openapigen.annotations.type.string.pattern + +import com.papsign.ktor.openapigen.model.schema.SchemaModel + +object RegularExpressionProcessor : RegularExpressionConstraintProcessor() { + override fun process(model: SchemaModel.SchemaModelLitteral<*>, annotation: RegularExpression): SchemaModel.SchemaModelLitteral<*> { + @Suppress("UNCHECKED_CAST") + return (model as SchemaModel.SchemaModelLitteral).apply { + pattern = annotation.pattern + } + } + + override fun getConstraint(annotation: RegularExpression): RegularExpressionConstraint { + return RegularExpressionConstraint(annotation.pattern, annotation.errorMessage) + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/papsign/ktor/openapigen/model/schema/SchemaModel.kt b/src/main/kotlin/com/papsign/ktor/openapigen/model/schema/SchemaModel.kt index f5f3671f2..05f743b24 100644 --- a/src/main/kotlin/com/papsign/ktor/openapigen/model/schema/SchemaModel.kt +++ b/src/main/kotlin/com/papsign/ktor/openapigen/model/schema/SchemaModel.kt @@ -55,6 +55,9 @@ sealed class SchemaModel: DataModel { var nullable: Boolean = false, var minimum: T? = null, var maximum: T? = null, + var minLength: Int? = null, + var maxLength: Int? = null, + var pattern: String? = null, override var example: T? = null, override var examples: List? = null, override var description: String? = null diff --git a/src/test/kotlin/TestServer.kt b/src/test/kotlin/TestServer.kt index 249f09c49..7b16fa227 100644 --- a/src/test/kotlin/TestServer.kt +++ b/src/test/kotlin/TestServer.kt @@ -18,6 +18,17 @@ import com.papsign.ktor.openapigen.annotations.parameters.PathParam import com.papsign.ktor.openapigen.annotations.properties.description.Description import com.papsign.ktor.openapigen.annotations.type.`object`.example.ExampleProvider import com.papsign.ktor.openapigen.annotations.type.`object`.example.WithExample +import com.papsign.ktor.openapigen.annotations.type.common.ConstraintViolation +import com.papsign.ktor.openapigen.annotations.type.number.floating.clamp.FClamp +import com.papsign.ktor.openapigen.annotations.type.number.floating.max.FMax +import com.papsign.ktor.openapigen.annotations.type.number.integer.clamp.Clamp +import com.papsign.ktor.openapigen.annotations.type.number.integer.max.Max +import com.papsign.ktor.openapigen.annotations.type.number.integer.min.Min +import com.papsign.ktor.openapigen.annotations.type.string.example.StringExample +import com.papsign.ktor.openapigen.annotations.type.string.length.Length +import com.papsign.ktor.openapigen.annotations.type.string.length.MaxLength +import com.papsign.ktor.openapigen.annotations.type.string.length.MinLength +import com.papsign.ktor.openapigen.annotations.type.string.pattern.RegularExpression import com.papsign.ktor.openapigen.interop.withAPI import com.papsign.ktor.openapigen.model.Described import com.papsign.ktor.openapigen.model.server.ServerModel @@ -28,11 +39,6 @@ import com.papsign.ktor.openapigen.route.path.normal.post import com.papsign.ktor.openapigen.route.response.respond import com.papsign.ktor.openapigen.schema.namer.DefaultSchemaNamer import com.papsign.ktor.openapigen.schema.namer.SchemaNamer -import com.papsign.ktor.openapigen.annotations.type.number.ConstraintVialoation -import com.papsign.ktor.openapigen.annotations.type.number.integer.clamp.Clamp -import com.papsign.ktor.openapigen.annotations.type.number.integer.max.Max -import com.papsign.ktor.openapigen.annotations.type.number.integer.min.Min -import com.papsign.ktor.openapigen.annotations.type.string.example.StringExample import io.ktor.application.application import io.ktor.application.call import io.ktor.application.install @@ -110,7 +116,7 @@ object TestServer { it.printStackTrace() Error("mapping.json", it.localizedMessage) } - exception(HttpStatusCode.BadRequest) { + exception(HttpStatusCode.BadRequest) { Error("violation.constraint", it.localizedMessage) } exception(HttpStatusCode.BadRequest) { @@ -187,6 +193,30 @@ object TestServer { respond(LongResponse(params.a)) } + route("validate-string").post( + info("This endpoint demonstrates the usage of String validators", "This endpoint demonstrates the usage of String validators"), + exampleRequest = StringValidatorsExample( + "A string that is at least 2 characters long", + "A short string", + "Between 2 and 20", + "5a21be2"), + exampleResponse = StringResponse("All of the fields were valid") + ) { params, body -> + respond(StringResponse("All of the fields were valid")) + } + + route("validate-number").post( + info("This endpoint demonstrates the usage of number validators", "This endpoint demonstrates the usage of number validators"), + exampleRequest = NumberValidatorsExample( + 1, + 56, + 15.02f, + 0.023f), + exampleResponse = StringResponse("All of the fields were valid") + ) { params, body -> + respond(StringResponse("All of the fields were valid")) + } + route("again") { tag(TestServer.Tags.EXAMPLE) { @@ -231,6 +261,21 @@ object TestServer { @Response("A Response for header param example") data class NameGreetingResponse(@StringExample("Hi, John!") val str: String) + @Request("A Request with String fields validated for length or pattern") + data class StringValidatorsExample( + @MinLength(2,"Optional custom error message") val strWithMinLength: String, + @MaxLength( 20 ) val strWithMaxLength: String, + @Length(2, 20 ) val strWithLength: String, + @RegularExpression("^[0-9a-fA-F]*$", "The field strHexaDec should only contain hexadecimal digits") val strHexaDec: String + ) + + @Request("A Request with validated number fields") + data class NumberValidatorsExample( + @Min(0, "The value of field intWithMin should be a positive integer") val intWithMin: Int, + @Clamp( 1, 90 ) val intBetween: Int, + @FMax(100.0) val floatMax: Float, + @FClamp(0.0, 1.0, "The value of field floatBetween should be a between 0 and 1") val floatBetween: Float + ) @Response("A String Response") @Request("A String Request") @@ -252,10 +297,10 @@ object TestServer { class A(val str: String) : Base() - class B(val i: @Min(0) @Max(2) Int) : Base() + class B(@Min(0) @Max(2) val i: Int) : Base() @WithExample - class C(val l: @Clamp(0, 10) Long) : Base() { + class C( @Clamp(0, 10) val l: Long) : Base() { companion object: ExampleProvider { override val example: C? = C(5) }