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

m6l3 - Validation #26

Merged
merged 1 commit into from
Jun 18, 2024
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
16 changes: 16 additions & 0 deletions lessons/m6l3-konform/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
plugins {
kotlin("jvm")
}

val coroutinesVersion: String by project

dependencies {
implementation(kotlin("stdlib"))

testImplementation(kotlin("test"))
testImplementation(kotlin("test-junit"))

testImplementation("io.konform:konform:0.4.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
}
61 changes: 61 additions & 0 deletions lessons/m6l3-konform/src/test/kotlin/KonformTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package ru.otus.otuskotlin.marketplace.lib.koform

import io.konform.validation.Invalid
import io.konform.validation.Validation
import io.konform.validation.ValidationBuilder
import io.konform.validation.jsonschema.pattern
import kotlinx.datetime.*
import org.junit.Test
import kotlin.test.assertContains
import kotlin.test.assertIs
import kotlin.time.Duration.Companion.days

// Konform - DSL для валидации
class KonformTest {
@Test
fun konform() {
val objValidator = Validation<SomeObject> {
SomeObject::userId {
pattern("^[0-9a-zA-Z_-]{1,64}\$")
}
SomeObject::dob {
minAge(15)
}
}

val resultUserId = objValidator.validate(SomeObject())
assertIs<Invalid<SomeObject>>(resultUserId)
val errorUserId = resultUserId.errors.firstOrNull { it.dataPath == ".userId" }
assertContains(errorUserId?.message ?: "", "pattern")
println("RESULT: ${errorUserId?.dataPath} ${errorUserId?.message}")

val resultAge = objValidator.validate(SomeObject(userId = "987987987"))
assertIs<Invalid<SomeObject>>(resultAge)
val errorAge = resultAge.errors.firstOrNull { it.dataPath == ".dob" }
assertContains(errorAge?.message ?: "", "Age")
println("RESULT: ${errorAge?.dataPath} ${errorAge?.message}")
}
}

private fun ValidationBuilder<LocalDate?>.minAge(age: Int) = addConstraint(
errorMessage = "Age cannot be in range of 15..130 years",
"15..130"
) {
if (it == null) {
println("EMPTY!")
false
} else {
val date = Clock.System
.now()
.minus((age * 365).days)
.toLocalDateTime(TimeZone.currentSystemDefault())
.date
println("DATE: $date ~ $it ${it.compareTo(date)}")
(it.compareTo(date) in 0..1)
}
}

data class SomeObject(
val userId: String = "",
val dob: LocalDate? = null,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package ru.otus.otuskotlin.marketplace.api.v2.mappers

import ru.otus.otuskotlin.marketplace.api.v2.models.*
import ru.otus.otuskotlin.marketplace.common.MkplContext
import ru.otus.otuskotlin.marketplace.common.models.*
import ru.otus.otuskotlin.marketplace.common.stubs.MkplStubs

// Демонстрация форматной валидации в мапере
private sealed interface Result<T,E>
private data class Ok<T,E>(val value: T) : Result<T,E>
private data class Err<T,E>(val errors: List<E>) : Result<T,E> {
constructor(error: E) : this(listOf(error))
}

private fun <T,E> Result<T,E>.getOrExec(default: T, block: (Err<T,E>) -> Unit = {}): T = when (this) {
is Ok<T,E> -> this.value
is Err<T,E> -> {
block(this)
default
}
}

@Suppress("unused")
private fun <T,E> Result<T,E>.getOrNull(block: (Err<T,E>) -> Unit = {}): T? = when (this) {
is Ok<T,E> -> this.value
is Err<T,E> -> {
block(this)
null
}
}

private fun String?.transportToStubCaseValidated(): Result<MkplStubs,MkplError> = when (this) {
"success" -> Ok(MkplStubs.SUCCESS)
"notFound" -> Ok(MkplStubs.NOT_FOUND)
"badId" -> Ok(MkplStubs.BAD_ID)
"badTitle" -> Ok(MkplStubs.BAD_TITLE)
"badDescription" -> Ok(MkplStubs.BAD_DESCRIPTION)
"badVisibility" -> Ok(MkplStubs.BAD_VISIBILITY)
"cannotDelete" -> Ok(MkplStubs.CANNOT_DELETE)
"badSearchString" -> Ok(MkplStubs.BAD_SEARCH_STRING)
null -> Ok(MkplStubs.NONE)
else -> Err(
MkplError(
code = "wrong-stub-case",
group = "mapper-validation",
field = "debug.stub",
message = "Unsupported value for case \"$this\""
)
)
}

@Suppress("unused")
fun MkplContext.fromTransportValidated(request: AdCreateRequest) {
command = MkplCommand.CREATE
// Вся магия здесь!
stubCase = request
.debug
?.stub
?.value
.transportToStubCaseValidated()
.getOrExec(MkplStubs.NONE) { err: Err<MkplStubs,MkplError> ->
errors.addAll(err.errors)
state = MkplState.FAILING
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package ru.otus.otuskotlin.marketplace.api.v2.mappers

import ru.otus.otuskotlin.marketplace.api.v2.models.AdCreateRequest
import ru.otus.otuskotlin.marketplace.api.v2.models.AdDebug
import ru.otus.otuskotlin.marketplace.api.v2.models.AdRequestDebugStubs
import ru.otus.otuskotlin.marketplace.common.MkplContext
import ru.otus.otuskotlin.marketplace.common.stubs.MkplStubs
import kotlin.test.Test
import kotlin.test.assertEquals

class MapperValidatedTest {
@Test
fun fromTransportValidated() {
val req = AdCreateRequest(
debug = AdDebug(
stub = AdRequestDebugStubs.SUCCESS,
),
)

val context = MkplContext()
context.fromTransportValidated(req)

assertEquals(MkplStubs.SUCCESS, context.stubCase)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,14 @@ import ru.otus.otuskotlin.marketplace.biz.general.initStatus
import ru.otus.otuskotlin.marketplace.biz.general.operation
import ru.otus.otuskotlin.marketplace.biz.general.stubs
import ru.otus.otuskotlin.marketplace.biz.stubs.*
import ru.otus.otuskotlin.marketplace.biz.validation.*
import ru.otus.otuskotlin.marketplace.common.MkplContext
import ru.otus.otuskotlin.marketplace.common.MkplCorSettings
import ru.otus.otuskotlin.marketplace.common.models.MkplAdId
import ru.otus.otuskotlin.marketplace.common.models.MkplAdLock
import ru.otus.otuskotlin.marketplace.common.models.MkplCommand
import ru.otus.otuskotlin.marketplace.cor.rootChain
import ru.otus.otuskotlin.marketplace.cor.worker

class MkplAdProcessor(
private val corSettings: MkplCorSettings = MkplCorSettings.NONE
Expand All @@ -25,6 +29,18 @@ class MkplAdProcessor(
stubDbError("Имитация ошибки работы с БД")
stubNoCase("Ошибка: запрошенный стаб недопустим")
}
validation {
worker("Копируем поля в adValidating") { adValidating = adRequest.deepCopy() }
worker("Очистка id") { adValidating.id = MkplAdId.NONE }
worker("Очистка заголовка") { adValidating.title = adValidating.title.trim() }
worker("Очистка описания") { adValidating.description = adValidating.description.trim() }
validateTitleNotEmpty("Проверка, что заголовок не пуст")
validateTitleHasContent("Проверка символов")
validateDescriptionNotEmpty("Проверка, что описание не пусто")
validateDescriptionHasContent("Проверка символов")

finishAdValidation("Завершение проверок")
}
}
operation("Получить объявление", MkplCommand.READ) {
stubs("Обработка стабов") {
Expand All @@ -33,6 +49,14 @@ class MkplAdProcessor(
stubDbError("Имитация ошибки работы с БД")
stubNoCase("Ошибка: запрошенный стаб недопустим")
}
validation {
worker("Копируем поля в adValidating") { adValidating = adRequest.deepCopy() }
worker("Очистка id") { adValidating.id = MkplAdId(adValidating.id.asString().trim()) }
validateIdNotEmpty("Проверка на непустой id")
validateIdProperFormat("Проверка формата id")

finishAdValidation("Успешное завершение процедуры валидации")
}
}
operation("Изменить объявление", MkplCommand.UPDATE) {
stubs("Обработка стабов") {
Expand All @@ -43,6 +67,23 @@ class MkplAdProcessor(
stubDbError("Имитация ошибки работы с БД")
stubNoCase("Ошибка: запрошенный стаб недопустим")
}
validation {
worker("Копируем поля в adValidating") { adValidating = adRequest.deepCopy() }
worker("Очистка id") { adValidating.id = MkplAdId(adValidating.id.asString().trim()) }
worker("Очистка lock") { adValidating.lock = MkplAdLock(adValidating.lock.asString().trim()) }
worker("Очистка заголовка") { adValidating.title = adValidating.title.trim() }
worker("Очистка описания") { adValidating.description = adValidating.description.trim() }
validateIdNotEmpty("Проверка на непустой id")
validateIdProperFormat("Проверка формата id")
validateLockNotEmpty("Проверка на непустой lock")
validateLockProperFormat("Проверка формата lock")
validateTitleNotEmpty("Проверка на непустой заголовок")
validateTitleHasContent("Проверка на наличие содержания в заголовке")
validateDescriptionNotEmpty("Проверка на непустое описание")
validateDescriptionHasContent("Проверка на наличие содержания в описании")

finishAdValidation("Успешное завершение процедуры валидации")
}
}
operation("Удалить объявление", MkplCommand.DELETE) {
stubs("Обработка стабов") {
Expand All @@ -51,6 +92,18 @@ class MkplAdProcessor(
stubDbError("Имитация ошибки работы с БД")
stubNoCase("Ошибка: запрошенный стаб недопустим")
}
validation {
worker("Копируем поля в adValidating") {
adValidating = adRequest.deepCopy()
}
worker("Очистка id") { adValidating.id = MkplAdId(adValidating.id.asString().trim()) }
worker("Очистка lock") { adValidating.lock = MkplAdLock(adValidating.lock.asString().trim()) }
validateIdNotEmpty("Проверка на непустой id")
validateIdProperFormat("Проверка формата id")
validateLockNotEmpty("Проверка на непустой lock")
validateLockProperFormat("Проверка формата lock")
finishAdValidation("Успешное завершение процедуры валидации")
}
}
operation("Поиск объявлений", MkplCommand.SEARCH) {
stubs("Обработка стабов") {
Expand All @@ -59,6 +112,12 @@ class MkplAdProcessor(
stubDbError("Имитация ошибки работы с БД")
stubNoCase("Ошибка: запрошенный стаб недопустим")
}
validation {
worker("Копируем поля в adFilterValidating") { adFilterValidating = adFilterRequest.deepCopy() }
validateSearchStringLength("Валидация длины строки поиска в фильтре")

finishAdFilterValidation("Успешное завершение процедуры валидации")
}
}
operation("Поиск подходящих предложений для объявления", MkplCommand.OFFERS) {
stubs("Обработка стабов") {
Expand All @@ -67,6 +126,15 @@ class MkplAdProcessor(
stubDbError("Имитация ошибки работы с БД")
stubNoCase("Ошибка: запрошенный стаб недопустим")
}
validation {
worker("Копируем поля в adValidating") { adValidating = adRequest.deepCopy() }
worker("Очистка id") { adValidating.id = MkplAdId(adValidating.id.asString().trim()) }
validateIdNotEmpty("Проверка на непустой id")
validateIdProperFormat("Проверка формата id")

finishAdValidation("Успешное завершение процедуры валидации")
}
}
}.build()
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package ru.otus.otuskotlin.marketplace.biz.validation

import ru.otus.otuskotlin.marketplace.common.MkplContext
import ru.otus.otuskotlin.marketplace.common.models.MkplState
import ru.otus.otuskotlin.marketplace.cor.ICorChainDsl
import ru.otus.otuskotlin.marketplace.cor.worker

fun ICorChainDsl<MkplContext>.finishAdValidation(title: String) = worker {
this.title = title
on { state == MkplState.RUNNING }
handle {
adValidated = adValidating
}
}

fun ICorChainDsl<MkplContext>.finishAdFilterValidation(title: String) = worker {
this.title = title
on { state == MkplState.RUNNING }
handle {
adFilterValidated = adFilterValidating
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package ru.otus.otuskotlin.marketplace.biz.validation

import ru.otus.otuskotlin.marketplace.common.helpers.errorValidation
import ru.otus.otuskotlin.marketplace.common.MkplContext
import ru.otus.otuskotlin.marketplace.common.helpers.fail
import ru.otus.otuskotlin.marketplace.cor.ICorChainDsl
import ru.otus.otuskotlin.marketplace.cor.worker

// пример обработки ошибки в рамках бизнес-цепочки
fun ICorChainDsl<MkplContext>.validateDescriptionHasContent(title: String) = worker {
this.title = title
val regExp = Regex("\\p{L}")
on { adValidating.description.isNotEmpty() && !adValidating.description.contains(regExp) }
handle {
fail(
errorValidation(
field = "description",
violationCode = "noContent",
description = "field must contain letters"
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package ru.otus.otuskotlin.marketplace.biz.validation

import ru.otus.otuskotlin.marketplace.cor.ICorChainDsl
import ru.otus.otuskotlin.marketplace.cor.worker
import ru.otus.otuskotlin.marketplace.common.helpers.errorValidation
import ru.otus.otuskotlin.marketplace.common.MkplContext
import ru.otus.otuskotlin.marketplace.common.helpers.fail

fun ICorChainDsl<MkplContext>.validateDescriptionNotEmpty(title: String) = worker {
this.title = title
on { adValidating.description.isEmpty() }
handle {
fail(
errorValidation(
field = "description",
violationCode = "empty",
description = "field must not be empty"
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package ru.otus.otuskotlin.marketplace.biz.validation

import ru.otus.otuskotlin.marketplace.cor.ICorChainDsl
import ru.otus.otuskotlin.marketplace.cor.worker
import ru.otus.otuskotlin.marketplace.common.helpers.errorValidation
import ru.otus.otuskotlin.marketplace.common.MkplContext
import ru.otus.otuskotlin.marketplace.common.helpers.fail

fun ICorChainDsl<MkplContext>.validateIdNotEmpty(title: String) = worker {
this.title = title
on { adValidating.id.asString().isEmpty() }
handle {
fail(
errorValidation(
field = "id",
violationCode = "empty",
description = "field must not be empty"
)
)
}
}
Loading
Loading