Skip to content

Commit

Permalink
feat: Support custom metadata property
Browse files Browse the repository at this point in the history
  • Loading branch information
alphaho committed Feb 2, 2025
1 parent 4ca8f56 commit 5bf89c3
Show file tree
Hide file tree
Showing 39 changed files with 611 additions and 483 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,10 @@ import arrow.core.raise.either
import dev.nesk.akkurate.ValidationResult
import dev.nesk.akkurate.constraints.ConstraintViolation
import dev.nesk.akkurate.constraints.ConstraintViolationSet
import dev.nesk.akkurate.constraints.GenericConstraintViolation
import dev.nesk.akkurate.constraints.GenericConstraintViolationSet

private fun ConstraintViolationSet.toNonEmptySet(): NonEmptySet<ConstraintViolation> =
private fun <E> GenericConstraintViolationSet<E>.toNonEmptySet(): NonEmptySet<GenericConstraintViolation<E>> =
nonEmptySetOf(this.first(), *this.drop(1).toTypedArray())

/**
Expand Down Expand Up @@ -52,7 +54,7 @@ private fun ConstraintViolationSet.toNonEmptySet(): NonEmptySet<ConstraintViolat
* normalizeBookName(" ") // Returns: "<error: Must not be blank>"
* ```
*/
public fun <T> ValidationResult<T>.toEither(): Either<NonEmptySet<ConstraintViolation>, T> = when (this) {
public fun <E, T> ValidationResult<E, T>.toEither(): Either<NonEmptySet<GenericConstraintViolation<E>>, T> = when (this) {
is ValidationResult.Failure -> violations.toNonEmptySet().left()
is ValidationResult.Success -> value.right()
}
Expand Down Expand Up @@ -85,7 +87,7 @@ public fun <T> ValidationResult<T>.toEither(): Either<NonEmptySet<ConstraintViol
*
* @return The validated value on successful result.
*/
public fun <T> Raise<NonEmptySet<ConstraintViolation>>.bind(validationResult: ValidationResult<T>): T = when (validationResult) {
public fun <E, T> Raise<NonEmptySet<GenericConstraintViolation<E>>>.bind(validationResult: ValidationResult<E, T>): T = when (validationResult) {
is ValidationResult.Failure -> raise(validationResult.violations.toNonEmptySet())
is ValidationResult.Success -> validationResult.value
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import dev.nesk.akkurate.ValidationResult
import dev.nesk.akkurate.Validator
import dev.nesk.akkurate.constraints.ConstraintViolation
import dev.nesk.akkurate.constraints.builders.isIdenticalTo
import dev.nesk.akkurate.validatables.DefaultMetadataType
import kotlin.test.Test
import kotlin.test.assertIs
import kotlin.test.assertSame
Expand Down Expand Up @@ -59,7 +60,7 @@ class ArrowKtTest {
val validationResult = validate(SomeSealedClass.Invalid)
val validationEither = validationResult.toEither()

assertIs<ValidationResult.Failure>(validationResult)
assertIs<ValidationResult.Failure<DefaultMetadataType>>(validationResult)
assertIs<Either.Left<NonEmptySet<ConstraintViolation>>>(validationEither, "The `Either` instance is a `Left` type")
assertSame(
validationResult.violations.single(),
Expand Down Expand Up @@ -87,7 +88,7 @@ class ArrowKtTest {
val validationResult = validate(SomeSealedClass.Invalid)
val validationEither = either { bind(validationResult) }

assertIs<ValidationResult.Failure>(validationResult)
assertIs<ValidationResult.Failure<DefaultMetadataType>>(validationResult)
assertIs<Either.Left<NonEmptySet<ConstraintViolation>>>(validationEither, "The `Either` instance is a `Left` type")
assertSame(
validationResult.violations.single(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

package dev.nesk.akkurate

import dev.nesk.akkurate.validatables.DefaultMetadataType
import dev.nesk.akkurate.validatables.Validatable

/**
Expand Down Expand Up @@ -62,13 +63,13 @@ internal class AkkurateScratch {
public fun <ValueType> Validator(
configuration: Configuration = Configuration(),
block: Validatable<ValueType>.() -> Unit,
): Validator.Runner<ValueType> = ValidatorWrapper(dev.nesk.akkurate.Validator(configuration, block))
): Validator.Runner<ValueType, DefaultMetadataType> = ValidatorWrapper(dev.nesk.akkurate.Validator(configuration, block))
}

public class ValidatorWrapper<ValueType>(
private val delegate: Validator.Runner<ValueType>,
) : Validator.Runner<ValueType> by delegate {
override fun invoke(value: ValueType): ValidationResult<ValueType> {
private val delegate: Validator.Runner<ValueType, DefaultMetadataType>,
) : Validator.Runner<ValueType, DefaultMetadataType> by delegate {
override fun invoke(value: ValueType): ValidationResult<DefaultMetadataType, ValueType> {
return delegate(value).also { result ->
when (result) {
is ValidationResult.Success -> println("Success")
Expand Down
5 changes: 3 additions & 2 deletions akkurate-core/src/commonMain/kotlin/dev/nesk/akkurate/Path.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@

package dev.nesk.akkurate

import dev.nesk.akkurate.validatables.GenericValidatable
import dev.nesk.akkurate.validatables.Validatable

public typealias Path = List<String>

public class PathBuilder(private val validatable: Validatable<*>) {
public class PathBuilder<MetadataType>(private val validatable: GenericValidatable<*, MetadataType>) {
public fun absolute(vararg pathSegments: String): Path = pathSegments.toList()

public fun relative(vararg pathSegments: String): Path {
Expand All @@ -32,4 +33,4 @@ public class PathBuilder(private val validatable: Validatable<*>) {
public fun appended(vararg pathSegments: String): Path = validatable.path() + pathSegments.toList()
}

public fun Validatable<*>.path(block: PathBuilder.() -> Path): Path = PathBuilder(this).block()
public fun <MetadataType> GenericValidatable<*, MetadataType>.path(block: PathBuilder<MetadataType>.() -> Path): Path = PathBuilder(this).block()
Original file line number Diff line number Diff line change
Expand Up @@ -20,16 +20,20 @@ package dev.nesk.akkurate
import dev.nesk.akkurate.ValidationResult.Failure
import dev.nesk.akkurate.ValidationResult.Success
import dev.nesk.akkurate.constraints.ConstraintViolationSet
import dev.nesk.akkurate.constraints.GenericConstraintViolationSet
import dev.nesk.akkurate.validatables.DefaultMetadataType

/**
* The result of a validation. Can be [a successful outcome][Success] with the validated value, or [a failure][Failure] with a violation list.
*/
public sealed interface ValidationResult<out T> {
public sealed interface ValidationResult<out E, out T> {
/**
* Throws an [Exception] if the result is a failure.
*/
public fun orThrow()

public fun orThrow(toDefaultMetadata: (E) -> DefaultMetadataType)

/**
* A successful outcome to the validation, with [the validated value][value].
*/
Expand All @@ -38,13 +42,14 @@ public sealed interface ValidationResult<out T> {
* The subject of the validation result.
*/
public val value: T,
) : ValidationResult<T> {
) : ValidationResult<Nothing, T> {
/**
* Returns the [value].
*/
public operator fun component1(): T = value

override fun orThrow(): Unit = Unit
override fun orThrow(toDefaultMetadata: (Nothing) -> DefaultMetadataType): Unit = Unit

override fun equals(other: Any?): Boolean {
if (this === other) return true
Expand All @@ -64,30 +69,36 @@ public sealed interface ValidationResult<out T> {
/**
* A failed outcome to the validation, with a violation list describing what went wrong.
*/
public class Failure internal constructor(
public class Failure<E> internal constructor(
/**
* A list of failed constraint violations.
*/
public val violations: ConstraintViolationSet,
) : ValidationResult<Nothing> {
public val violations: GenericConstraintViolationSet<E>,
) : ValidationResult<E, Nothing> {
/**
* Returns the [violations].
*/
public operator fun component1(): ConstraintViolationSet = violations
public operator fun component1(): GenericConstraintViolationSet<E> = violations

override fun orThrow(): Nothing = orThrow { emptyMap() }
override fun orThrow(toDefaultMetadata: (E) -> DefaultMetadataType): Nothing =
throw Exception(
ConstraintViolationSet(violations.map { it.copy(metadata = toDefaultMetadata(it.metadata)) }.toSet())
)

override fun orThrow(): Nothing = throw Exception(violations)

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false

other as Failure
other as Failure<E>

return violations == other.violations
}

override fun hashCode(): Int = violations.hashCode()
override fun toString(): String = "ValidationResult.Failure(violations=$violations)"

}

/**
Expand Down
Loading

0 comments on commit 5bf89c3

Please sign in to comment.