cleaner rule matching implementation based on Either

This commit is contained in:
octarine-noise
2021-07-15 00:50:46 +02:00
parent 8ef84718b5
commit 4637e282ce
7 changed files with 354 additions and 300 deletions

View File

@@ -1,117 +1,157 @@
package mods.betterfoliage.config.match
typealias RuleLogConsumer = (ConfigSource, String) -> Unit
sealed class MatchValue(val description: String) {
class Found<T>(description: String, val value: T) : MatchValue(description)
class Missing(description: String) : MatchValue(description)
class Invalid(description: String) : MatchValue(description)
}
sealed class MatchResult {
abstract val isSuccess: Boolean
abstract val isInvariant: Boolean
abstract val configSource: ConfigSource
abstract fun log(logger: RuleLogConsumer)
class UniComparison(
override val isSuccess: Boolean,
override val isInvariant: Boolean,
override val configSource: ConfigSource,
val sourceValue: MatchValue,
val targetValue: String,
val matchMethod: MatchMethod
) : MatchResult() {
override fun log(logger: RuleLogConsumer) = logger(
configSource,
"${sourceValue.description} ${matchMethod.description(isSuccess)} \"$targetValue\""
)
}
class BiComparison(
override val isSuccess: Boolean,
override val isInvariant: Boolean,
override val configSource: ConfigSource,
val source: MatchValue,
val target: MatchValue,
val matchMethod: MatchMethod
) : MatchResult() {
override fun log(logger: RuleLogConsumer) = logger(
configSource,
"${source.description} ${matchMethod.description(isSuccess)} ${target.description}"
)
}
class InvalidValue(override val configSource: ConfigSource, val value: MatchValue, val description: String) : MatchResult() {
override val isSuccess = false
override val isInvariant = true
override fun log(logger: RuleLogConsumer) = logger(configSource, description)
}
class Error(override val configSource: ConfigSource, val description: String) : MatchResult() {
override val isSuccess = false
override val isInvariant = true
override fun log(logger: RuleLogConsumer) = logger(configSource, description)
}
class Action(override val configSource: ConfigSource, val description: String) : MatchResult() {
override val isSuccess = true
override val isInvariant = false
override fun log(logger: RuleLogConsumer) = logger(configSource, description)
}
class Any(override val configSource: ConfigSource, val results: List<MatchResult>) : MatchResult() {
override val isSuccess = results.any(MatchResult::isSuccess)
override val isInvariant = results.all(MatchResult::isInvariant)
override fun log(logger: RuleLogConsumer) {
val toLog = if (results.any { it.isSuccess }) results.filter { it.isSuccess } else results
toLog.forEach { it.log(logger) }
}
}
class RootList(override val configSource: ConfigSource, val results: List<MatchResult>) : MatchResult() {
override val isSuccess = results.all(MatchResult::isSuccess)
override val isInvariant = results.all(MatchResult::isInvariant)
override fun log(logger: RuleLogConsumer) {
results.forEach { it.log(logger) }
}
}
}
fun Node.HasSource.error(description: String) = MatchResult.Error(configSource, description)
fun Node.HasSource.notImplemented() = MatchResult.Error(configSource, "match type not implemented: ${this::class.java.name}")
fun Node.HasSource.action(description: String) = MatchResult.Action(configSource, description)
fun Node.HasSource.invalidValue(value: MatchValue) = MatchResult.InvalidValue(
configSource, value, "invalid value: ${value.description}"
)
fun Node.HasSource.invalidValueType(comparisonLeft: String, value: Node.Value) = MatchResult.Error(
configSource, "invalid type for $comparisonLeft: [${value::class.java.name}] \"${value.value}\""
)
fun <T> Node.MatchValueList.compare(sourceValue: MatchValue, targetValue: Node.Value, func: (MatchValue.Found<T>) -> Boolean): MatchResult {
if (sourceValue is MatchValue.Missing || sourceValue is MatchValue.Invalid) return invalidValue(sourceValue)
val isSuccess = func(sourceValue as MatchValue.Found<T>)
return MatchResult.UniComparison(isSuccess, true, configSource, sourceValue, targetValue.value, matchMethod)
}
fun <T> Node.MatchValueList.compare(sourceValue: MatchValue, targetValue: MatchValue, func: (MatchValue.Found<T>, MatchValue.Found<T>) -> Boolean): MatchResult {
if (sourceValue is MatchValue.Missing || sourceValue is MatchValue.Invalid) return invalidValue(sourceValue)
if (targetValue is MatchValue.Missing || targetValue is MatchValue.Invalid) return invalidValue(targetValue)
val isSuccess = func(sourceValue as MatchValue.Found<T>, targetValue as MatchValue.Found<T>)
return MatchResult.BiComparison(isSuccess, true, configSource, sourceValue, targetValue, matchMethod)
}
enum class MatchMethod {
EXACT_MATCH, EXTENDS, CONTAINS;
fun description(isSuccess: Boolean) = when(this) {
fun description(isSuccess: Boolean) = when (this) {
EXACT_MATCH -> if (isSuccess) "matches" else "does not match"
EXTENDS -> if (isSuccess) "extends" else "does not extend"
CONTAINS -> if (isSuccess) "contains" else "does not contain"
}
}
/**
* Basic Either monad implementation
*/
sealed class Either<out L, out R> {
class Left<L>(val left: L) : Either<L, Nothing>()
class Right<R>(val right: R) : Either<Nothing, R>()
fun leftOrNull() = if (this is Left) left else null
fun rightOrNull() = if (this is Right) right else null
fun <R2> map(func: (R) -> R2): Either<L, R2> = when (this) {
is Left<L> -> this
is Right<R> -> Right(func(right))
}
fun <L2> mapLeft(func: (L) -> L2): Either<L2, R> = when (this) {
is Left<L> -> Left(func(left))
is Right<R> -> this
}
fun ifRight(action: (R) -> Unit) {
if (this is Right) action(right)
}
companion object {
fun <L> ofLeft(left: L) = Left(left)
fun <R> ofRight(right: R) = Right(right)
}
}
// this cannot be inside the class for variance reasons
fun <L, R, R2> Either<L, R>.flatMap(func: (R) -> Either<L, R2>) = when (this) {
is Either.Left<L> -> this
is Either.Right<R> -> func(right)
}
fun <L, R, L2> Either<L, R>.flatMapLeft(func: (L) -> Either<L2, R>) = when (this) {
is Either.Left<L> -> func(left)
is Either.Right<R> -> this
}
fun <T> Either<T, T>.flatten() = when (this) {
is Either.Left -> left
is Either.Right -> right
}
interface MAnything<out T> {
val value: T
val immutable: Boolean
}
class MListAll(val list: List<MAnything<Boolean>>) : MAnything<Boolean> {
override val value get() = list.all { it.value }
override val immutable get() = list.all { it.immutable }
}
class MListAny(val list: List<MValue<Boolean>>) : MAnything<Boolean> {
override val value get() = list.any { it.value }
override val immutable get() = list.all { it.immutable }
}
/**
* Value with metadata related to rule matching applied.
*
* @param value the wrapped value
* @param description human-readable description of what the value represents
* @param configSource identifies where the value is described in the config
* @param immutable true if the value never changes
* (another [MValue] constructed in the same way will have the same value)
*
*/
class MValue<out T>(
override val value: T,
val description: String,
val configSource: ConfigSource,
override val immutable: Boolean,
) : MAnything<T> {
companion object {
fun <T> right(value: T, description: String, configSource: ConfigSource, immutable: Boolean = true) =
Either.ofRight(MValue(value, description, configSource, immutable))
fun left(description: String, configSource: ConfigSource, immutable: Boolean = true) =
Either.ofLeft(MValue(false, description, configSource, immutable))
}
}
typealias MEither<T> = Either<MValue<Boolean>, MValue<T>>
val Node.Value.asEither get() = MValue.right(value, value, configSource, true)
fun Node.Value.left(description: String) = MValue.left(description, configSource)
fun Node.invalidTypeFor(type: String) = MValue.left("invalid type for $type: [${this::class.java.name}]", configSource)
fun Node.error(description: String) = MValue.left(description, configSource)
fun <T, R> MEither<T>.mapValue(func: (T) -> R) = map {
MValue(func(it.value), it.description, it.configSource, it.immutable)
}
fun <T> MEither<T>.mapDescription(func: (MValue<T>) -> String) = map {
MValue(it.value, func(it), it.configSource, it.immutable)
}
fun <T, R> MEither<T>.map(
func: (T) -> R,
description: (MValue<T>, R) -> String
) = map { t -> func(t.value).let { r -> MValue(r, description(t, r), t.configSource, t.immutable) } }
fun <T, R> MEither<T>.mapNotNull(
func: (T) -> R?,
dLeft: (MValue<T>) -> String = { it.description },
dRight: (MValue<T>, R) -> String = { m, _ -> m.description }
) = flatMap { t ->
func(t.value)?.let { r ->
MValue.right(r, dRight(t, r), t.configSource, t.immutable)
} ?: MValue.left(dLeft(t), t.configSource, t.immutable)
}
fun <T> MEither<T>.toRight(value: T) =
flatMapLeft { MValue.right(value, it.description, it.configSource, it.immutable) }
data class MComparison<T1, T2>(
private val opTrue: String,
private val opFalse: String,
val testFunc: (T1, T2) -> Boolean
) {
fun compare(value1: MEither<T1>, value2: MEither<T2>) = when {
value1 is Either.Left -> value1
value2 is Either.Left -> value2
else -> {
val isSuccess = testFunc((value1 as Either.Right).right.value, (value2 as Either.Right).right.value)
MValue.right(
isSuccess,
"${value1.right.description} ${if (isSuccess) opTrue else opFalse} ${value2.right.description}",
value2.right.configSource,
value1.right.immutable && value2.right.immutable
)
}
}.flatten()
companion object {
fun <T1, T2> of(matchMethod: MatchMethod, testFunc: (T1, T2) -> Boolean) =
MComparison(matchMethod.description(true), matchMethod.description(false), testFunc)
val equals = of(MatchMethod.EXACT_MATCH) { t1: Any, t2: Any -> t1 == t2 }
}
}