Skip to content

Commit 2a57714

Browse files
committed
Add pattern validator with regex
1 parent 1a0f363 commit 2a57714

File tree

4 files changed

+88
-0
lines changed

4 files changed

+88
-0
lines changed
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package com.papsign.ktor.openapigen.annotations.type.string.pattern
2+
3+
import com.papsign.ktor.openapigen.schema.processor.SchemaProcessorAnnotation
4+
import com.papsign.ktor.openapigen.validation.ValidatorAnnotation
5+
6+
@Target(AnnotationTarget.TYPE, AnnotationTarget.PROPERTY)
7+
@SchemaProcessorAnnotation(RegularExpressionProcessor::class)
8+
@ValidatorAnnotation(RegularExpressionProcessor::class)
9+
annotation class RegularExpression(val pattern: String, val message: String = "")
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
package com.papsign.ktor.openapigen.annotations.type.string.pattern
2+
3+
import com.papsign.ktor.openapigen.classLogger
4+
import com.papsign.ktor.openapigen.getKType
5+
import com.papsign.ktor.openapigen.model.schema.SchemaModel
6+
import com.papsign.ktor.openapigen.schema.processor.SchemaProcessor
7+
import com.papsign.ktor.openapigen.validation.Validator
8+
import com.papsign.ktor.openapigen.validation.ValidatorBuilder
9+
import java.lang.Exception
10+
import kotlin.reflect.KType
11+
import kotlin.reflect.full.withNullability
12+
13+
abstract class RegularExpressionConstraintProcessor<A: Annotation>(): SchemaProcessor<A>, ValidatorBuilder<A> {
14+
15+
private val log = classLogger()
16+
17+
val types = listOf(getKType<String>().withNullability(true), getKType<String>().withNullability(false))
18+
19+
abstract fun process(model: SchemaModel.SchemaModelLitteral<*>, annotation: A): SchemaModel.SchemaModelLitteral<*>
20+
21+
abstract fun getConstraint(annotation: A): RegularExpressionConstraint
22+
23+
private class RegularExpressionConstraintValidator(private val constraint: RegularExpressionConstraint): Validator {
24+
override fun <T> validate(subject: T?): T? {
25+
if (subject is String?) {
26+
if (subject == null || !constraint.pattern.toRegex().containsMatchIn(subject)) {
27+
throw RegularExpressionConstraintViolation(subject, constraint)
28+
}
29+
} else {
30+
throw NotAStringViolation(subject)
31+
}
32+
return subject
33+
}
34+
}
35+
36+
override fun build(type: KType, annotation: A): Validator {
37+
return if (types.contains(type)) {
38+
RegularExpressionConstraintValidator(getConstraint(annotation))
39+
} else {
40+
error("${annotation::class} can only be used on types: $types")
41+
}
42+
}
43+
44+
override fun process(model: SchemaModel<*>, type: KType, annotation: A): SchemaModel<*> {
45+
return if (model is SchemaModel.SchemaModelLitteral<*> && types.contains(type)) {
46+
process(model, annotation)
47+
} else {
48+
log.warn("${annotation::class} can only be used on types: $types")
49+
model
50+
}
51+
}
52+
}
53+
54+
data class RegularExpressionConstraint(val pattern: String, val errorMessage: String? = null)
55+
56+
open class ConstraintViolation(message: String, cause: Throwable? = null): Exception(message, cause)
57+
58+
class RegularExpressionConstraintViolation(val actual: String?, val constraint: RegularExpressionConstraint): ConstraintViolation(constraint.errorMessage ?: "Constraint violation: the string " +
59+
"\'$actual\' does not match the regular expression ${constraint.pattern}")
60+
61+
class NotAStringViolation(val value: Any?): ConstraintViolation("Constraint violation: $value is not a string")
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package com.papsign.ktor.openapigen.annotations.type.string.pattern
2+
3+
import com.papsign.ktor.openapigen.model.schema.SchemaModel
4+
5+
object RegularExpressionProcessor : RegularExpressionConstraintProcessor<RegularExpression>() {
6+
override fun process(model: SchemaModel.SchemaModelLitteral<*>, annotation: RegularExpression): SchemaModel.SchemaModelLitteral<*> {
7+
@Suppress("UNCHECKED_CAST")
8+
return (model as SchemaModel.SchemaModelLitteral<Any?>).apply {
9+
pattern = annotation.pattern
10+
}
11+
}
12+
13+
override fun getConstraint(annotation: RegularExpression): RegularExpressionConstraint {
14+
val errorMessage = if (annotation.message.isNotEmpty()) annotation.message else null
15+
return RegularExpressionConstraint(annotation.pattern, errorMessage)
16+
}
17+
}

src/main/kotlin/com/papsign/ktor/openapigen/model/schema/SchemaModel.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ sealed class SchemaModel<T>: DataModel {
5757
var maximum: T? = null,
5858
var minLength: Int? = null,
5959
var maxLength: Int? = null,
60+
var pattern: String? = null,
6061
override var example: T? = null,
6162
override var examples: List<T>? = null,
6263
override var description: String? = null

0 commit comments

Comments
 (0)