Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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