Skip to content

Commit

Permalink
Merge pull request #57 from bargergo/string-length-validators
Browse files Browse the repository at this point in the history
Add validators for strings
  • Loading branch information
Wicpar authored Jun 3, 2020
2 parents 10f49e3 + 5874a1f commit 729aef7
Show file tree
Hide file tree
Showing 27 changed files with 315 additions and 27 deletions.
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -59,11 +59,9 @@ abstract class NumberConstraintProcessor<A: Annotation>(allowedTypes: Iterable<K
}
}

data class NumberConstraint(val min: BigDecimal? = null, val max: BigDecimal? = null, val minInclusive: Boolean = true, val maxInclusive: Boolean = true)
data class NumberConstraint(val min: BigDecimal? = null, val max: BigDecimal? = null, val minInclusive: Boolean = true, val maxInclusive: Boolean = true, val errorMessage: String)

open class ConstraintVialoation(message: String, cause: Throwable? = null): Exception(message, cause)

class NumberConstraintViolation(val actual: Number?, val constraint: NumberConstraint): ConstraintVialoation("Constraint violation: $actual should be ${
class NumberConstraintViolation(val actual: Number?, val constraint: NumberConstraint): ConstraintViolation("Constraint violation: $actual should be ${
{
val min = "${constraint.min} ${if (constraint.minInclusive) "inclusive" else "exclusive"}"
val max = "${constraint.max} ${if (constraint.maxInclusive) "inclusive" else "exclusive"}"
Expand All @@ -74,6 +72,6 @@ class NumberConstraintViolation(val actual: Number?, val constraint: NumberConst
else -> "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")
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "")


Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ object FClampProcessor : FloatingNumberConstraintProcessor<FClamp>() {
}

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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "")
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ object FMaxProcessor: FloatingNumberConstraintProcessor<FMax>() {
}
}
override fun getConstraint(annotation: FMax): NumberConstraint {
return NumberConstraint(max= BigDecimal(annotation.value))
return NumberConstraint(max= BigDecimal(annotation.value), errorMessage = annotation.errorMessage)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "")
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ object FMinProcessor: FloatingNumberConstraintProcessor<FMin>() {
}

override fun getConstraint(annotation: FMin): NumberConstraint {
return NumberConstraint(min = BigDecimal(annotation.value))
return NumberConstraint(min = BigDecimal(annotation.value), errorMessage = annotation.errorMessage)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "")
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,6 @@ object ClampProcessor: IntegerNumberConstraintProcessor<Clamp>() {
}

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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "")
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,6 @@ object MaxProcessor: IntegerNumberConstraintProcessor<Max>() {
}
}
override fun getConstraint(annotation: Max): NumberConstraint {
return NumberConstraint(max= BigDecimal(annotation.value))
return NumberConstraint(max= BigDecimal(annotation.value), errorMessage = annotation.errorMessage)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "")
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,6 @@ object MinProcessor: IntegerNumberConstraintProcessor<Min>() {
}

override fun getConstraint(annotation: Min): NumberConstraint {
return NumberConstraint(min = BigDecimal(annotation.value))
return NumberConstraint(min = BigDecimal(annotation.value), errorMessage = annotation.errorMessage)
}
}
Original file line number Diff line number Diff line change
@@ -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")
Original file line number Diff line number Diff line change
@@ -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 = "")
Original file line number Diff line number Diff line change
@@ -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<A: Annotation>(): SchemaProcessor<A>, ValidatorBuilder<A> {

private val log = classLogger()

val types = listOf(getKType<String>().withNullability(true), getKType<String>().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 <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 (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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.papsign.ktor.openapigen.annotations.type.string.length

import com.papsign.ktor.openapigen.model.schema.SchemaModel

object LengthProcessor : LengthConstraintProcessor<Length>() {
override fun process(model: SchemaModel.SchemaModelLitteral<*>, annotation: Length): SchemaModel.SchemaModelLitteral<*> {
@Suppress("UNCHECKED_CAST")
return (model as SchemaModel.SchemaModelLitteral<Any?>).apply {
maxLength = annotation.max
minLength = annotation.min
}
}

override fun getConstraint(annotation: Length): LengthConstraint {
return LengthConstraint(min = annotation.min, max = annotation.max, errorMessage = annotation.errorMessage)
}
}
Original file line number Diff line number Diff line change
@@ -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 = "")
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.papsign.ktor.openapigen.annotations.type.string.length

import com.papsign.ktor.openapigen.model.schema.SchemaModel

object MaxLengthProcessor : LengthConstraintProcessor<MaxLength>() {
override fun process(model: SchemaModel.SchemaModelLitteral<*>, annotation: MaxLength): SchemaModel.SchemaModelLitteral<*> {
@Suppress("UNCHECKED_CAST")
return (model as SchemaModel.SchemaModelLitteral<Any?>).apply {
maxLength = annotation.value
}
}

override fun getConstraint(annotation: MaxLength): LengthConstraint {
return LengthConstraint(max = annotation.value, errorMessage = annotation.errorMessage)
}
}
Original file line number Diff line number Diff line change
@@ -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 = "")
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.papsign.ktor.openapigen.annotations.type.string.length

import com.papsign.ktor.openapigen.model.schema.SchemaModel

object MinLengthProcessor : LengthConstraintProcessor<MinLength>() {
override fun process(model: SchemaModel.SchemaModelLitteral<*>, annotation: MinLength): SchemaModel.SchemaModelLitteral<*> {
@Suppress("UNCHECKED_CAST")
return (model as SchemaModel.SchemaModelLitteral<Any?>).apply {
minLength = annotation.value
}
}

override fun getConstraint(annotation: MinLength): LengthConstraint {
return LengthConstraint(min = annotation.value, errorMessage = annotation.errorMessage)
}
}
Original file line number Diff line number Diff line change
@@ -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 = "")
Original file line number Diff line number Diff line change
@@ -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<A: Annotation>(): SchemaProcessor<A>, ValidatorBuilder<A> {

private val log = classLogger()

val types = listOf(getKType<String>().withNullability(true), getKType<String>().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 <T> 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)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.papsign.ktor.openapigen.annotations.type.string.pattern

import com.papsign.ktor.openapigen.model.schema.SchemaModel

object RegularExpressionProcessor : RegularExpressionConstraintProcessor<RegularExpression>() {
override fun process(model: SchemaModel.SchemaModelLitteral<*>, annotation: RegularExpression): SchemaModel.SchemaModelLitteral<*> {
@Suppress("UNCHECKED_CAST")
return (model as SchemaModel.SchemaModelLitteral<Any?>).apply {
pattern = annotation.pattern
}
}

override fun getConstraint(annotation: RegularExpression): RegularExpressionConstraint {
return RegularExpressionConstraint(annotation.pattern, annotation.errorMessage)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ sealed class SchemaModel<T>: 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<T>? = null,
override var description: String? = null
Expand Down
Loading

0 comments on commit 729aef7

Please sign in to comment.