diff --git a/app/build.gradle.kts b/app/build.gradle.kts index b3218768..e579a1f9 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -158,9 +158,10 @@ dependencies { implementation("androidx.room:room-runtime:2.3.0") kapt("androidx.room:room-compiler:2.3.0") - testImplementation("io.kotest:kotest-runner-junit5:5.0.0.M2") - testImplementation("io.kotest:kotest-assertions-core:5.0.0.M2") - testImplementation("io.kotest:kotest-property:5.0.0.M2") + testImplementation("io.kotest:kotest-runner-junit5:4.6.3") + testImplementation("io.kotest:kotest-assertions-core:4.6.3") + testImplementation("io.kotest:kotest-property:4.6.3") + testImplementation("io.kotest.extensions:kotest-extensions-robolectric:0.4.0") androidTestImplementation("androidx.test.ext", "junit", "1.1.2") androidTestImplementation("androidx.test.espresso", "espresso-core", "3.3.0") diff --git a/app/src/main/java/dev/patrickgold/florisboard/res/ext/ExtensionAuthor.kt b/app/src/main/java/dev/patrickgold/florisboard/res/ext/ExtensionAuthor.kt index 9a0f7cde..3ce8e903 100644 --- a/app/src/main/java/dev/patrickgold/florisboard/res/ext/ExtensionAuthor.kt +++ b/app/src/main/java/dev/patrickgold/florisboard/res/ext/ExtensionAuthor.kt @@ -27,25 +27,31 @@ import kotlinx.serialization.encoding.Encoder import kotlin.math.min @Serializable(with = ExtensionAuthorSerializer::class) -class ExtensionAuthor( +data class ExtensionAuthor( val name: String, val email: String? = null, val url: FlorisRef? = null, ) { companion object { + private val ValidationRegex = """^\s*[\p{L}\d._-][\p{L}\d\s._-]*(<[^<>]+>)?\s*(\([^()]+\))?\s*${'$'}""".toRegex() + fun from(str: String): ExtensionAuthor? { - if (str.isBlank()) { + if (str.isBlank() || !ValidationRegex.matches(str)) { return null } - val emailStart = str.indexOf('<').let { if (it < 0) str.length else it } + 1 - val emailEnd = str.indexOf('>').let { if (it < 0) str.length else it } - 1 - val urlStart = str.indexOf('(').let { if (it < 0) str.length else it } + 1 - val urlEnd = str.indexOf(')').let { if (it < 0) str.length else it } - 1 + val emailStart = str.indexOf('<').let { if (it < 0) str.length else (it + 1) } + val emailEnd = str.indexOf('>').let { if (it < 0) str.length else it } + val urlStart = str.indexOf('(').let { if (it < 0) str.length else (it + 1) } + val urlEnd = str.indexOf(')').let { if (it < 0) str.length else it } val nameStart = 0 - val nameEnd = (min(emailStart, urlStart) - 2).coerceIn(str.indices) - val name = str.substring(nameStart..nameEnd).trim() - val email = str.substring(emailStart..emailEnd).trim().takeIf { it.isNotBlank() } - val url = str.substring(urlStart..urlEnd).trim().takeIf { it.isNotBlank() } + val nameEnd = if (emailStart == str.length && urlStart == str.length) { + str.length + } else { + (min(emailStart, urlStart) - 1) + } + val name = str.substring(nameStart, nameEnd).trim() + val email = str.substring(emailStart, emailEnd).trim().takeIf { it.isNotBlank() } + val url = str.substring(urlStart, urlEnd).trim().takeIf { it.isNotBlank() } return ExtensionAuthor(name, email, url?.let { FlorisRef.from(it) }) } @@ -59,7 +65,7 @@ class ExtensionAuthor( if (email != null && email.isNotBlank()) { append(" <$email>") } - if (url != null && url.isValid) { + if (url != null) { append(" ($url)") } } diff --git a/app/src/test/java/dev/patrickgold/florisboard/res/ext/ExtensionAuthorTest.kt b/app/src/test/java/dev/patrickgold/florisboard/res/ext/ExtensionAuthorTest.kt new file mode 100644 index 00000000..f809a098 --- /dev/null +++ b/app/src/test/java/dev/patrickgold/florisboard/res/ext/ExtensionAuthorTest.kt @@ -0,0 +1,66 @@ +package dev.patrickgold.florisboard.res.ext + +import android.app.Application +import dev.patrickgold.florisboard.res.FlorisRef +import io.kotest.core.spec.style.FreeSpec +import io.kotest.extensions.robolectric.RobolectricTest +import io.kotest.matchers.shouldBe + +@RobolectricTest(application = Application::class) +class ExtensionAuthorTest : FreeSpec({ + val validAuthorPairs = listOf( + "Jane Doe" to ExtensionAuthor(name = "Jane Doe"), + "jane123" to ExtensionAuthor(name = "jane123"), + "__jane__" to ExtensionAuthor(name = "__jane__"), + "jane.doe" to ExtensionAuthor(name = "jane.doe"), + "Jane Doe " to ExtensionAuthor(name = "Jane Doe", email = "jane.doe@gmail.com"), + "Jane Doe (jane-doe.com)" to ExtensionAuthor(name = "Jane Doe", url = FlorisRef.from("jane-doe.com")), + "Jane Doe (jane-doe.com)" to ExtensionAuthor(name = "Jane Doe", email = "jane.doe@gmail.com", url = FlorisRef.from("jane-doe.com")), + ) + + "Test ExtensionAuthor.from()" - { + "With valid, well-formatted input" - { + validAuthorPairs.forEach { (authorStr, authorObj) -> + "`$authorStr`" { + ExtensionAuthor.from(authorStr) shouldBe authorObj + } + } + } + + "With valid, ill-formatted input" - { + listOf( + " Jane Doe " to ExtensionAuthor(name = "Jane Doe"), + " jane123" to ExtensionAuthor(name = "jane123"), + " Jane Doe " to ExtensionAuthor(name = "Jane Doe", email = "jane.doe@gmail.com"), + ).forEach { (authorStr, authorObj) -> + "`$authorStr`" { + ExtensionAuthor.from(authorStr) shouldBe authorObj + } + } + } + + "With invalid input" - { + listOf( + "", + " ", + "", + " ", + " (jane-doe.com)", + "Jane Doe <> ((jane-doe.com))", + "Jane Doe + "`$authorStr` should be null" { + ExtensionAuthor.from(authorStr) shouldBe null + } + } + } + } + + "Test ExtensionAuthor.toString()" - { + validAuthorPairs.forEach { (authorStr, authorObj) -> + "`$authorStr`" { + authorObj.toString() shouldBe authorStr + } + } + } +})