Skip to content

Commit

Permalink
Levenshtein distance calculation for the enhancement of the 'invalid …
Browse files Browse the repository at this point in the history
…enum' error (#232)

### What's done:
- Inspired by diktat: in mostly each and every configuration files we have enums and authors of libraries that work with this configuration file would like to help users to find a misprint and suggest the closest value
- we think that this should be done on the side of all decoding libraries
  • Loading branch information
orchestr7 authored Jun 18, 2023
1 parent 0f8cd0a commit 873ce99
Show file tree
Hide file tree
Showing 3 changed files with 105 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

package com.akuleshov7.ktoml.exceptions

import com.akuleshov7.ktoml.utils.closestEnumName
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.SerializationException
import kotlinx.serialization.descriptors.SerialDescriptor
Expand Down Expand Up @@ -34,8 +35,9 @@ internal class InvalidEnumValueException(
enumSerialDescriptor: SerialDescriptor,
lineNo: Int
) : TomlDecodingException(
"Value <$value> is not a valid enum option." +
" Permitted choices are: ${enumSerialDescriptor.elementNames.sorted().joinToString(", ")}"
"Line $lineNo: value <$value> is not a valid enum option." +
" Did you mean <${enumSerialDescriptor.elementNames.closestEnumName(value)}>?" +
" Permitted choices are: ${enumSerialDescriptor.elementNames.sorted().joinToString(", ")}."
)

internal class NullValueException(propertyName: String, lineNo: Int) : TomlDecodingException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,13 @@ import com.akuleshov7.ktoml.tree.nodes.TableType
import com.akuleshov7.ktoml.tree.nodes.TomlNode
import com.akuleshov7.ktoml.tree.nodes.TomlTable

/**
* @param enumValue input value that we need to compare with elements of enum
* @return nearest enum value (using levenshtein distance algorithm)
*/
public fun Iterable<String>.closestEnumName(enumValue: String): String? =
this.minByOrNull { levenshteinDistance(it, enumValue) }

/**
* Append a code point to a [StringBuilder]
*
Expand Down Expand Up @@ -35,3 +42,40 @@ public fun findPrimitiveTableInAstByName(children: List<TomlNode>, fullTableName

return findPrimitiveTableInAstByName(children.map { it.children }.flatten(), fullTableName)
}

/**
* Unfortunately Levenshtein method is implemented in jline and not ported to Kotlin Native.
* So we need to implement it (inspired by: https://pl.kotl.in/ifo0z0vMC)
*
* @param first string to compare
* @param second string for comparison
* @return the distance between compared strings
*/
public fun levenshteinDistance(first: String, second: String): Int {
when {
first == second -> return 0
first.isEmpty() -> return second.length
second.isEmpty() -> return first.length
else -> {
// this is a generated else block
}
}

val firstLen = first.length + 1
val secondLen = second.length + 1
var distance = IntArray(firstLen) { it }
var newDistance = IntArray(firstLen) { 0 }

for (i in 1 until secondLen) {
newDistance[0] = i
for (j in 1 until firstLen) {
val costReplace = distance[j - 1] + (if (first[j - 1] == second[i - 1]) 0 else 1)
val costInsert = distance[j] + 1
val costDelete = newDistance[j - 1] + 1

newDistance[j] = minOf(costInsert, costDelete, costReplace)
}
distance = newDistance.also { newDistance = distance }
}
return distance[firstLen - 1]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.akuleshov7.ktoml.decoders

import com.akuleshov7.ktoml.Toml
import com.akuleshov7.ktoml.exceptions.IllegalTypeException
import com.akuleshov7.ktoml.exceptions.InvalidEnumValueException
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertFailsWith

@Serializable
data class Color(val myEnum: EnumExample)

enum class EnumExample {
CANAPA,
KANAPA,
KANADA,
USA,
MEXICO,
}

class EnumValidationTest {
@Test
fun testRegressions() {
var exception = assertFailsWith<InvalidEnumValueException> {
Toml.decodeFromString<Color>("myEnum = \"KANATA\"")
}

exception.exceptionValidation(
"Line 1: value <KANATA> is not a valid enum option. Did you mean <KANAPA>? " +
"Permitted choices are: CANAPA, KANADA, KANAPA, MEXICO, USA."
)

exception = assertFailsWith<InvalidEnumValueException> {
Toml.decodeFromString<Color>("myEnum = \"TEST\"")
}

exception.exceptionValidation(
"Line 1: value <TEST> is not a valid enum option. Did you mean <USA>? " +
"Permitted choices are: CANAPA, KANADA, KANAPA, MEXICO, USA."
)

exception = assertFailsWith<InvalidEnumValueException> {
Toml.decodeFromString<Color>("myEnum = \"MEKSICA\"")
}

exception.exceptionValidation(
"Line 1: value <MEKSICA> is not a valid enum option. Did you mean <MEXICO>? " +
"Permitted choices are: CANAPA, KANADA, KANAPA, MEXICO, USA."
)
}
}

private fun InvalidEnumValueException.exceptionValidation(expected: String) {
assertEquals(expected, this.message)
}

0 comments on commit 873ce99

Please sign in to comment.