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

Support cross-field validation #98

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
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
35 changes: 33 additions & 2 deletions src/commonMain/kotlin/io/konform/validation/Validation.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,46 @@ import io.konform.validation.internal.ValidationBuilderImpl

public interface Validation<T> {
public companion object {
public operator fun <T> invoke(init: ValidationBuilder<T>.() -> Unit): Validation<T> {
public operator fun <T> invoke(init: ValidationBuilder<T>.(Context<T>) -> Unit): Validation<T> {
val context = Context<T>()
val builder = ValidationBuilderImpl<T>()
return builder.apply(init).build()
init(builder, context)
val validation = builder.build()
return object : Validation<T> by validation {
override fun validate(value: T): ValidationResult<T> {
context.subject = value
return validation.validate(value)
}

override fun invoke(value: T): ValidationResult<T> {
return validate(value)
}
}
}
}

public fun validate(value: T): ValidationResult<T>

public operator fun invoke(value: T): ValidationResult<T> = validate(value)

public class Context<T> {
private var _subjectHolder: SubjectHolder<T>? = null

public var subject: T
get() {
return when (val subjectHolder = _subjectHolder) {
null -> throw IllegalStateException("Subject not initialized")
else -> subjectHolder.value
}
}
set(value) {
_subjectHolder = SubjectHolder(value)
}

public operator fun component1(): T = subject

private data class SubjectHolder<T>(val value: T)
}
}

public class Constraint<R> internal constructor(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,32 @@ class ValidationBuilderTest {
Register(referredBy = "poweruser@").let { assertEquals(1, countErrors(nullableFieldValidation(it), Register::referredBy)) }
}

@Test
fun validatingFieldsWithContext() {
val fieldValidation =
Validation<Register> { context ->
Register::password {
addConstraint("cannot equal email") { it != context.subject.email }
}
}

Register(email = "sillyuser@test.com", password = "sillyuser@test.com")
.let { assertEquals(1, countErrors(fieldValidation(it), Register::password)) }
}

@Test
fun validatingFieldsWithDestructuredContext() {
val fieldValidation =
Validation<Register> { (register) ->
Register::password {
addConstraint("cannot equal email") { it != register.email }
}
}

Register(email = "sillyuser@test.com", password = "sillyuser@test.com")
.let { assertEquals(1, countErrors(fieldValidation(it), Register::password)) }
}

@Test
fun validatingNestedTypesDirectly() {
val nestedTypeValidation =
Expand All @@ -130,6 +156,17 @@ class ValidationBuilderTest {
Register(home = Address("")).let { assertEquals(1, countErrors(nestedTypeValidation(it), Register::home, Address::address)) }
}

@Test
fun validatingNullableValues() {
val nullableValueValidation =
Validation<String?> {
addConstraint("cannot be null") { it != null}
}

"poweruser@test.com".let { assertEquals(Valid(it), nullableValueValidation(it)) }
null.let { assertEquals(1, countErrors(nullableValueValidation(it))) }
}

@Test
fun validatingOptionalNullableValues() {
val nullableTypeValidation =
Expand Down
Loading