Repo created
This commit is contained in:
parent
a629de6271
commit
3cef7c5092
2161 changed files with 246605 additions and 2 deletions
9
core/common/build.gradle.kts
Normal file
9
core/common/build.gradle.kts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
@Suppress("DSL_SCOPE_VIOLATION")
|
||||
plugins {
|
||||
id(ThunderbirdPlugins.Library.jvm)
|
||||
alias(libs.plugins.android.lint)
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testImplementation(projects.core.testing)
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package app.k9mail.core.common
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.dsl.module
|
||||
|
||||
val coreCommonModule: Module = module {
|
||||
single<Clock> { Clock.System }
|
||||
}
|
||||
12
core/common/src/main/kotlin/app/k9mail/core/common/cache/Cache.kt
vendored
Normal file
12
core/common/src/main/kotlin/app/k9mail/core/common/cache/Cache.kt
vendored
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
package app.k9mail.core.common.cache
|
||||
|
||||
interface Cache<KEY : Any, VALUE : Any?> {
|
||||
|
||||
operator fun get(key: KEY): VALUE?
|
||||
|
||||
operator fun set(key: KEY, value: VALUE)
|
||||
|
||||
fun hasKey(key: KEY): Boolean
|
||||
|
||||
fun clear()
|
||||
}
|
||||
46
core/common/src/main/kotlin/app/k9mail/core/common/cache/ExpiringCache.kt
vendored
Normal file
46
core/common/src/main/kotlin/app/k9mail/core/common/cache/ExpiringCache.kt
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
package app.k9mail.core.common.cache
|
||||
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
class ExpiringCache<KEY : Any, VALUE : Any?>(
|
||||
private val clock: Clock,
|
||||
private val delegateCache: Cache<KEY, VALUE> = InMemoryCache(),
|
||||
private var lastClearTime: Instant = clock.now(),
|
||||
private val cacheTimeValidity: Long = CACHE_TIME_VALIDITY_IN_MILLIS,
|
||||
) : Cache<KEY, VALUE> {
|
||||
|
||||
override fun get(key: KEY): VALUE? {
|
||||
recycle()
|
||||
return delegateCache[key]
|
||||
}
|
||||
|
||||
override fun set(key: KEY, value: VALUE) {
|
||||
recycle()
|
||||
delegateCache[key] = value
|
||||
}
|
||||
|
||||
override fun hasKey(key: KEY): Boolean {
|
||||
recycle()
|
||||
return delegateCache.hasKey(key)
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
lastClearTime = clock.now()
|
||||
delegateCache.clear()
|
||||
}
|
||||
|
||||
private fun recycle() {
|
||||
if (isExpired()) {
|
||||
clear()
|
||||
}
|
||||
}
|
||||
|
||||
private fun isExpired(): Boolean {
|
||||
return (clock.now() - lastClearTime).inWholeMilliseconds >= cacheTimeValidity
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val CACHE_TIME_VALIDITY_IN_MILLIS = 30_000L
|
||||
}
|
||||
}
|
||||
21
core/common/src/main/kotlin/app/k9mail/core/common/cache/InMemoryCache.kt
vendored
Normal file
21
core/common/src/main/kotlin/app/k9mail/core/common/cache/InMemoryCache.kt
vendored
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
package app.k9mail.core.common.cache
|
||||
|
||||
class InMemoryCache<KEY : Any, VALUE : Any?>(
|
||||
private val cache: MutableMap<KEY, VALUE> = mutableMapOf(),
|
||||
) : Cache<KEY, VALUE> {
|
||||
override fun get(key: KEY): VALUE? {
|
||||
return cache[key]
|
||||
}
|
||||
|
||||
override fun set(key: KEY, value: VALUE) {
|
||||
cache[key] = value
|
||||
}
|
||||
|
||||
override fun hasKey(key: KEY): Boolean {
|
||||
return cache.containsKey(key)
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
cache.clear()
|
||||
}
|
||||
}
|
||||
30
core/common/src/main/kotlin/app/k9mail/core/common/cache/SynchronizedCache.kt
vendored
Normal file
30
core/common/src/main/kotlin/app/k9mail/core/common/cache/SynchronizedCache.kt
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package app.k9mail.core.common.cache
|
||||
|
||||
class SynchronizedCache<KEY : Any, VALUE : Any?>(
|
||||
private val delegateCache: Cache<KEY, VALUE>,
|
||||
) : Cache<KEY, VALUE> {
|
||||
|
||||
override fun get(key: KEY): VALUE? {
|
||||
synchronized(delegateCache) {
|
||||
return delegateCache[key]
|
||||
}
|
||||
}
|
||||
|
||||
override fun set(key: KEY, value: VALUE) {
|
||||
synchronized(delegateCache) {
|
||||
delegateCache[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
override fun hasKey(key: KEY): Boolean {
|
||||
synchronized(delegateCache) {
|
||||
return delegateCache.hasKey(key)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clear() {
|
||||
synchronized(delegateCache) {
|
||||
delegateCache.clear()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
package app.k9mail.core.common.mail
|
||||
|
||||
@JvmInline
|
||||
value class EmailAddress(val address: String) {
|
||||
init {
|
||||
require(address.isNotBlank()) { "Email address must not be blank" }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
package app.k9mail.core.common
|
||||
|
||||
import org.junit.Test
|
||||
import org.koin.dsl.koinApplication
|
||||
import org.koin.test.check.checkModules
|
||||
|
||||
internal class CoreCommonModuleKtTest {
|
||||
|
||||
@Test
|
||||
fun `should have a valid di module`() {
|
||||
koinApplication {
|
||||
modules(coreCommonModule)
|
||||
checkModules()
|
||||
}
|
||||
}
|
||||
}
|
||||
81
core/common/src/test/kotlin/app/k9mail/core/common/cache/CacheTest.kt
vendored
Normal file
81
core/common/src/test/kotlin/app/k9mail/core/common/cache/CacheTest.kt
vendored
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
package app.k9mail.core.common.cache
|
||||
|
||||
import app.k9mail.core.testing.TestClock
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isNull
|
||||
import assertk.assertions.isTrue
|
||||
import kotlin.test.Test
|
||||
import org.junit.runner.RunWith
|
||||
import org.junit.runners.Parameterized
|
||||
|
||||
data class CacheTestData<KEY : Any, VALUE : Any?>(
|
||||
val name: String,
|
||||
val createCache: () -> Cache<KEY, VALUE>
|
||||
) {
|
||||
override fun toString(): String = name
|
||||
}
|
||||
|
||||
@RunWith(Parameterized::class)
|
||||
class CacheTest(data: CacheTestData<Any, Any?>) {
|
||||
|
||||
private val testSubject = data.createCache()
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
@Parameterized.Parameters(name = "{0}")
|
||||
fun data(): Collection<CacheTestData<Any, Any?>> {
|
||||
return listOf(
|
||||
CacheTestData("InMemoryCache") { InMemoryCache() },
|
||||
CacheTestData("ExpiringCache") { ExpiringCache(TestClock(), InMemoryCache()) },
|
||||
CacheTestData("SynchronizedCache") { SynchronizedCache(InMemoryCache()) }
|
||||
)
|
||||
}
|
||||
|
||||
const val KEY = "key"
|
||||
const val VALUE = "value"
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `get should return null with empty cache`() {
|
||||
assertThat(testSubject[KEY]).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `set should add entry with empty cache`() {
|
||||
testSubject[KEY] = VALUE
|
||||
|
||||
assertThat(testSubject[KEY]).isEqualTo(VALUE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `set should overwrite entry when already present`() {
|
||||
testSubject[KEY] = VALUE
|
||||
|
||||
testSubject[KEY] = "$VALUE changed"
|
||||
|
||||
assertThat(testSubject[KEY]).isEqualTo("$VALUE changed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasKey should answer no with empty cache`() {
|
||||
assertThat(testSubject.hasKey(KEY)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasKey should answer yes when cache has entry`() {
|
||||
testSubject[KEY] = VALUE
|
||||
|
||||
assertThat(testSubject.hasKey(KEY)).isTrue()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `clear should empty cache`() {
|
||||
testSubject[KEY] = VALUE
|
||||
|
||||
testSubject.clear()
|
||||
|
||||
assertThat(testSubject[KEY]).isNull()
|
||||
}
|
||||
}
|
||||
68
core/common/src/test/kotlin/app/k9mail/core/common/cache/ExpiringCacheTest.kt
vendored
Normal file
68
core/common/src/test/kotlin/app/k9mail/core/common/cache/ExpiringCacheTest.kt
vendored
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
package app.k9mail.core.common.cache
|
||||
|
||||
import app.k9mail.core.testing.TestClock
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import assertk.assertions.isFalse
|
||||
import assertk.assertions.isNull
|
||||
import kotlin.test.Test
|
||||
import kotlin.time.Duration.Companion.milliseconds
|
||||
|
||||
class ExpiringCacheTest {
|
||||
|
||||
private val clock = TestClock()
|
||||
|
||||
private val testSubject: Cache<String, String> = ExpiringCache(clock, InMemoryCache())
|
||||
|
||||
@Test
|
||||
fun `get should return null when entry present and cache expired`() {
|
||||
testSubject[KEY] = VALUE
|
||||
clock.advanceTimeBy(CACHE_TIME_VALIDITY_DURATION)
|
||||
|
||||
val result = testSubject[KEY]
|
||||
|
||||
assertThat(result).isNull()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `set should clear cache and add new entry when cache expired`() {
|
||||
testSubject[KEY] = VALUE
|
||||
clock.advanceTimeBy(CACHE_TIME_VALIDITY_DURATION)
|
||||
|
||||
testSubject[KEY + 1] = "$VALUE changed"
|
||||
|
||||
assertThat(testSubject[KEY]).isNull()
|
||||
assertThat(testSubject[KEY + 1]).isEqualTo("$VALUE changed")
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `hasKey should answer no when cache has entry and validity expired`() {
|
||||
testSubject[KEY] = VALUE
|
||||
clock.advanceTimeBy(CACHE_TIME_VALIDITY_DURATION)
|
||||
|
||||
assertThat(testSubject.hasKey(KEY)).isFalse()
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should keep cache when time progresses within expiration`() {
|
||||
testSubject[KEY] = VALUE
|
||||
clock.advanceTimeBy(CACHE_TIME_VALIDITY_DURATION.minus(1L.milliseconds))
|
||||
|
||||
assertThat(testSubject[KEY]).isEqualTo(VALUE)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should empty cache after time progresses to expiration`() {
|
||||
testSubject[KEY] = VALUE
|
||||
|
||||
clock.advanceTimeBy(CACHE_TIME_VALIDITY_DURATION)
|
||||
|
||||
assertThat(testSubject[KEY]).isNull()
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val KEY = "key"
|
||||
const val VALUE = "value"
|
||||
val CACHE_TIME_VALIDITY_DURATION = 30_000L.milliseconds
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package app.k9mail.core.common.mail
|
||||
|
||||
import assertk.assertThat
|
||||
import assertk.assertions.isEqualTo
|
||||
import kotlin.test.Test
|
||||
import kotlin.test.assertFails
|
||||
|
||||
internal class EmailAddressTest {
|
||||
|
||||
@Test
|
||||
fun `should reject blank email address`() {
|
||||
assertFails("Email address must not be blank") {
|
||||
EmailAddress("")
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `should return email address`() {
|
||||
val emailAddress = EmailAddress(EMAIL_ADDRESS)
|
||||
|
||||
val address = emailAddress.address
|
||||
|
||||
assertThat(address).isEqualTo(EMAIL_ADDRESS)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
private const val EMAIL_ADDRESS = "email@example.com"
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue