From 80d95e8f052ddda36671e20911ef56659bd83667 Mon Sep 17 00:00:00 2001 From: Kittinun Vantasin Date: Mon, 11 Apr 2022 10:44:02 +0900 Subject: [PATCH] [New version] Update with new Kotlin version and cleanup on the interface (#62) --- .../fuse/android/FuseByteCacheTest.kt | 3 +- .../com/github/kittinunf/fuse/core/Cache.kt | 56 +-------- .../com/github/kittinunf/fuse/core/Config.kt | 7 +- .../com/github/kittinunf/fuse/core/Fuse.kt | 91 ++++++++++++++- .../kittinunf/fuse/core/fetch/DiskFetcher.kt | 5 +- .../kittinunf/fuse/core/fetch/Fetcher.kt | 9 +- .../fuse/core/scenario/ExpirableCache.kt | 40 ++++--- .../com/github/kittinunf/fuse/util/MD5.kt | 2 +- .../kittinunf/fuse/FuseByteCacheTest.kt | 3 +- ...ioTest.kt => FuseExpirableScenarioTest.kt} | 52 ++++++--- .../github/kittinunf/fuse/NetworkFetcher.kt | 2 +- gradle/libs.versions.toml | 15 +-- gradle/wrapper/gradle-wrapper.properties | 2 +- sample/build.gradle.kts | 2 - .../github/kittinunf/fuse/sample/LocalTime.kt | 19 ++- .../fuse/sample/LocalTimeRepository.kt | 102 +++++++++-------- .../kittinunf/fuse/sample/LocalTimeService.kt | 108 +++++++++--------- .../kittinunf/fuse/sample/NetworkFetcher.kt | 53 +++++++++ .../fuse/sample/view/MainActivity.kt | 32 +++--- .../ic_launcher_foreground.xml | 0 sample/src/main/res/layout/activity_main.xml | 32 +++--- .../res/mipmap-anydpi-v26/ic_launcher.xml | 5 - .../mipmap-anydpi-v26/ic_launcher_round.xml | 5 - settings.gradle.kts | 1 - 24 files changed, 383 insertions(+), 263 deletions(-) rename fuse/src/test/java/com/github/kittinunf/fuse/{FuseScenarioTest.kt => FuseExpirableScenarioTest.kt} (87%) create mode 100644 sample/src/main/java/com/github/kittinunf/fuse/sample/NetworkFetcher.kt rename sample/src/main/res/{drawable-v24 => drawable}/ic_launcher_foreground.xml (100%) delete mode 100644 sample/src/main/res/mipmap-anydpi-v26/ic_launcher.xml delete mode 100644 sample/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml diff --git a/fuse-android/src/test/java/com/github/kittinunf/fuse/android/FuseByteCacheTest.kt b/fuse-android/src/test/java/com/github/kittinunf/fuse/android/FuseByteCacheTest.kt index a6de62e..1b56884 100644 --- a/fuse-android/src/test/java/com/github/kittinunf/fuse/android/FuseByteCacheTest.kt +++ b/fuse-android/src/test/java/com/github/kittinunf/fuse/android/FuseByteCacheTest.kt @@ -168,8 +168,9 @@ class FuseByteCacheTest : BaseTestCase() { val timestamp = cache.getTimestamp("timestamp") + assertThat(timestamp, notNullValue()) assertThat(timestamp, not(equalTo(-1L))) - assertThat(System.currentTimeMillis() - timestamp, object : BaseMatcher() { + assertThat(System.currentTimeMillis() - timestamp!!, object : BaseMatcher() { override fun describeTo(description: Description?) {} override fun matches(item: Any?): Boolean { diff --git a/fuse/src/main/java/com/github/kittinunf/fuse/core/Cache.kt b/fuse/src/main/java/com/github/kittinunf/fuse/core/Cache.kt index 8b69736..f51df1a 100644 --- a/fuse/src/main/java/com/github/kittinunf/fuse/core/Cache.kt +++ b/fuse/src/main/java/com/github/kittinunf/fuse/core/Cache.kt @@ -1,31 +1,18 @@ package com.github.kittinunf.fuse.core import com.github.kittinunf.fuse.core.cache.Entry -import com.github.kittinunf.fuse.core.fetch.DiskFetcher import com.github.kittinunf.fuse.core.fetch.Fetcher -import com.github.kittinunf.fuse.core.fetch.NoFetcher -import com.github.kittinunf.fuse.core.fetch.SimpleFetcher import com.github.kittinunf.fuse.util.md5 import com.github.kittinunf.result.Result import com.github.kittinunf.result.flatMap -import java.io.File object CacheBuilder { - fun config( - dir: String, - convertible: Fuse.DataConvertible, - construct: Config.() -> Unit = {} - ): Config { + fun config(dir: String, convertible: Fuse.DataConvertible, construct: Config.() -> Unit = {}): Config { return Config(dir, convertible = convertible).apply(construct) } - fun config( - dir: String, - name: String, - convertible: Fuse.DataConvertible, - construct: Config.() -> Unit = {} - ): Config { + fun config(dir: String, name: String, convertible: Fuse.DataConvertible, construct: Config.() -> Unit = {}): Config { return Config(dir, name, convertible).apply(construct) } } @@ -38,11 +25,7 @@ enum class Source { DISK, } -interface Cache : - Fuse.Cacheable, - Fuse.Cacheable.Put, - Fuse.Cacheable.Get, - Fuse.DataConvertible +interface Cache : Fuse.Cacheable, Fuse.Cacheable.Put, Fuse.Cacheable.Get, Fuse.DataConvertible class CacheImpl internal constructor( private val config: Config @@ -147,9 +130,9 @@ class CacheImpl internal constructor( return value != null } - override fun getTimestamp(key: String): Long { + override fun getTimestamp(key: String): Long? { val safeKey = key.md5() - return memCache.getTimestamp(safeKey) ?: diskCache.getTimestamp(safeKey) ?: -1 + return memCache.getTimestamp(safeKey) ?: diskCache.getTimestamp(safeKey) ?: null } private fun fetchAndPut(fetcher: Fetcher): Result { @@ -157,32 +140,3 @@ class CacheImpl internal constructor( return fetchResult.flatMap { put(fetcher.key, it) } } } - -// region File -fun Cache.get(file: File): Result = get(DiskFetcher(file, this)) - -fun Cache.getWithSource(file: File): Pair, Source> = - getWithSource(DiskFetcher(file, this)) - -fun Cache.put(file: File): Result = put(DiskFetcher(file, this)) -// endregion File - -// region Value -fun Cache.get(key: String, getValue: (() -> T?)? = null): Result { - val fetcher = if (getValue == null) NoFetcher(key) else SimpleFetcher(key, getValue) - return get(fetcher) -} - -fun Cache.getWithSource( - key: String, - getValue: (() -> T?)? = null -): Pair, Source> { - val fetcher = if (getValue == null) NoFetcher(key) else SimpleFetcher(key, getValue) - return getWithSource(fetcher) -} - -fun Cache.put(key: String, putValue: T? = null): Result { - val fetcher = if (putValue == null) NoFetcher(key) else SimpleFetcher(key, { putValue }) - return put(fetcher) -} -// endregion Value diff --git a/fuse/src/main/java/com/github/kittinunf/fuse/core/Config.kt b/fuse/src/main/java/com/github/kittinunf/fuse/core/Config.kt index 1df670a..4115729 100644 --- a/fuse/src/main/java/com/github/kittinunf/fuse/core/Config.kt +++ b/fuse/src/main/java/com/github/kittinunf/fuse/core/Config.kt @@ -21,9 +21,6 @@ class Config( } internal fun defaultMemoryCache(minimalSize: Int = 128): Persistence = MemCache(minimalSize) + internal fun defaultDiskCache(cacheDir: String, name: String, diskCapacity: Long): Persistence = - DiskCache.open( - cacheDir, - name, - diskCapacity - ) + DiskCache.open(cacheDir, name, diskCapacity) diff --git a/fuse/src/main/java/com/github/kittinunf/fuse/core/Fuse.kt b/fuse/src/main/java/com/github/kittinunf/fuse/core/Fuse.kt index 0c72ca3..4e3f043 100644 --- a/fuse/src/main/java/com/github/kittinunf/fuse/core/Fuse.kt +++ b/fuse/src/main/java/com/github/kittinunf/fuse/core/Fuse.kt @@ -1,9 +1,13 @@ package com.github.kittinunf.fuse.core +import com.github.kittinunf.fuse.core.fetch.DiskFetcher import com.github.kittinunf.fuse.core.fetch.Fetcher +import com.github.kittinunf.fuse.core.fetch.NeverFetcher +import com.github.kittinunf.fuse.core.fetch.SimpleFetcher import com.github.kittinunf.result.Result +import java.io.File -class Fuse { +object Fuse { interface DataConvertible { fun convertFromData(bytes: ByteArray): T @@ -77,8 +81,89 @@ class Fuse { /** * Retrieve the keys from all values persisted * @param key The key associated with the object to be persisted - * @return Long represents the timestamp in milliseconds since epoch 1970 + * @return Long represents the timestamp in milliseconds since epoch 1970, or null which means that the key is not present in cache */ - fun getTimestamp(key: String): Long + fun getTimestamp(key: String): Long? } } + +// region File +/** + * Get the entry associated as a Data of file content in T with its particular key as File path. If File is not there or too large, it returns as [Result.Failure] + * Otherwise, it returns [Result.Success] of data of a given file in T + * + * @param file The file object that represent file data on the disk + * @return Result The Result that represents the success/failure of the operation + */ +fun Cache.get(file: File): Result = get(DiskFetcher(file, this)) + +/** + * Get the entry associated as a Data of file content in T with its particular key as File path. If File is not there or too large, it returns as [Result.Failure] + * Otherwise, it returns [Result.Success] data of a given file in T + * + * @param file The file object that represent file data on the disk + * @return Pair, Source>> The Result that represents the success/failure of the operation + */ +fun Cache.getWithSource(file: File): Pair, Source> = + getWithSource(DiskFetcher(file, this)) + +/** + * Put the entry as a content of a file into Cache + * + * @param file The file object that represent file data on the disk + * @return Result The Result that represents the success/failure of the operation + */ +fun Cache.put(file: File): Result = put(DiskFetcher(file, this)) +// endregion File + +// region Value +/** + * Get the entry associated as a value in T by using lambda getValue as a default value generator. If value for associated Key is not there, it saves with value from defaultValue. + * + * @param key The String represent key of the entry + * @return Result The Result that represents the success/failure of the operation + */ +fun Cache.get(key: String, defaultValue: (() -> T?)): Result { + val fetcher = SimpleFetcher(key, defaultValue) + return get(fetcher) +} + +/** + * Get the entry associated as a value in T. Unlike [Cache.get(key: String, defaultValue: (() -> T))] counterpart, if value for associated Key is not there, it returns as [Result.Failure] + * + * @param key The String represent key of the entry + * @return Result The Result that represents the success/failure of the operation + */ +fun Cache.get(key: String): Result = get(NeverFetcher(key)) + +/** + * Get the entry associated as a value in T by using lambda as a default value generator. if value for associated key is not there, it saves with value from defaultValue. + * + * @param key The string represent key of the entry + * @return Pair, Source>> The result that represents the success/failure of the operation + */ +fun Cache.getWithSource(key: String, getValue: (() -> T?)): Pair, Source> { + val fetcher = SimpleFetcher(key, getValue) + return getWithSource(fetcher) +} + +/** + * Get the entry associated as a value in T by using lambda as a default value generator. if value for associated key is not there, it saves with value from defaultValue. + * + * @param key The string represent key of the entry + * @return Pair, Source>> The result that represents the success/failure of the operation + */ +fun Cache.getWithSource(key: String): Pair, Source> = getWithSource(NeverFetcher(key)) + +/** + * Put the entry as a content of a file into Cache + * + * @param key file object that represent file data on the disk + * @return Result The Result that represents the success/failure of the operation + */ +fun Cache.put(key: String, putValue: T): Result { + val fetcher = SimpleFetcher(key, { putValue }) + return put(fetcher) +} +// endregion Value + diff --git a/fuse/src/main/java/com/github/kittinunf/fuse/core/fetch/DiskFetcher.kt b/fuse/src/main/java/com/github/kittinunf/fuse/core/fetch/DiskFetcher.kt index d94c85d..d4c6e50 100644 --- a/fuse/src/main/java/com/github/kittinunf/fuse/core/fetch/DiskFetcher.kt +++ b/fuse/src/main/java/com/github/kittinunf/fuse/core/fetch/DiskFetcher.kt @@ -5,8 +5,7 @@ import com.github.kittinunf.result.Result import com.github.kittinunf.result.map import java.io.File -class DiskFetcher(private val file: File, private val convertible: Fuse.DataConvertible) : - Fetcher, +class DiskFetcher(private val file: File, private val convertible: Fuse.DataConvertible) : Fetcher, Fuse.DataConvertible by convertible { override val key: String = file.path @@ -15,7 +14,7 @@ class DiskFetcher(private val file: File, private val convertible: Fuse override fun fetch(): Result { val readFileResult = Result.of { file.readBytes() } - if (cancelled) return Result.error(RuntimeException("Fetch got cancelled")) + if (cancelled) return Result.failure(RuntimeException("Fetch got cancelled")) return readFileResult.map { convertFromData(it) } } diff --git a/fuse/src/main/java/com/github/kittinunf/fuse/core/fetch/Fetcher.kt b/fuse/src/main/java/com/github/kittinunf/fuse/core/fetch/Fetcher.kt index 0758189..0c93680 100644 --- a/fuse/src/main/java/com/github/kittinunf/fuse/core/fetch/Fetcher.kt +++ b/fuse/src/main/java/com/github/kittinunf/fuse/core/fetch/Fetcher.kt @@ -12,14 +12,15 @@ interface Fetcher { } } -class NotFoundException(key: String) : RuntimeException("value from $key is not found") +class NotFoundException(key: String) : RuntimeException("Value with key: $key is not found in cache") internal class SimpleFetcher(override val key: String, private val getValue: () -> T?) : Fetcher { - override fun fetch(): Result = Result.of(getValue(), { NotFoundException(key) }) + override fun fetch(): Result = + if (getValue() == null) Result.failure(RuntimeException("Fetch with Key: $key is failure")) else Result.of(getValue) } -internal class NoFetcher(override val key: String) : Fetcher { +internal class NeverFetcher(override val key: String) : Fetcher { - override fun fetch(): Result = Result.error(NotFoundException(key)) + override fun fetch(): Result = Result.failure(NotFoundException(key)) } diff --git a/fuse/src/main/java/com/github/kittinunf/fuse/core/scenario/ExpirableCache.kt b/fuse/src/main/java/com/github/kittinunf/fuse/core/scenario/ExpirableCache.kt index 64f0d40..bd83113 100644 --- a/fuse/src/main/java/com/github/kittinunf/fuse/core/scenario/ExpirableCache.kt +++ b/fuse/src/main/java/com/github/kittinunf/fuse/core/scenario/ExpirableCache.kt @@ -1,16 +1,15 @@ package com.github.kittinunf.fuse.core.scenario import com.github.kittinunf.fuse.core.Cache -import com.github.kittinunf.fuse.core.Fuse import com.github.kittinunf.fuse.core.Source import com.github.kittinunf.fuse.core.fetch.Fetcher -import com.github.kittinunf.fuse.core.fetch.NoFetcher +import com.github.kittinunf.fuse.core.fetch.NeverFetcher import com.github.kittinunf.fuse.core.fetch.SimpleFetcher import com.github.kittinunf.result.Result import kotlin.time.Duration -import kotlin.time.ExperimentalTime +import kotlin.time.Duration.Companion.milliseconds -class ExpirableCache(private val cache: Cache) : Fuse.Cacheable by cache, Fuse.Cacheable.Put by cache { +class ExpirableCache(private val cache: Cache) : Cache by cache { /** * Get the entry associated with its particular key which provided by the persistence. @@ -26,7 +25,6 @@ class ExpirableCache(private val cache: Cache) : Fuse.Cacheable by c * @param useEntryEvenIfExpired The flag indicates whether we still want to use the entry or not * @return Result The Result that represents the success/failure of the operation */ - @OptIn(ExperimentalTime::class) fun get( fetcher: Fetcher, timeLimit: Duration = Duration.INFINITE, @@ -47,7 +45,6 @@ class ExpirableCache(private val cache: Cache) : Fuse.Cacheable by c * @param useEntryEvenIfExpired The flag indicates whether we still want to use the entry or not * @return Pair, Cache.Source> The Pair of the result that represents the success/failure of the operation and The source of the entry */ - @OptIn(ExperimentalTime::class) fun getWithSource( fetcher: Fetcher, timeLimit: Duration = Duration.INFINITE, @@ -57,7 +54,7 @@ class ExpirableCache(private val cache: Cache) : Fuse.Cacheable by c val persistedTimestamp = getTimestamp(key) // no timestamp fetch, we need to just fetch the new data - return if (persistedTimestamp == -1L) { + return if (persistedTimestamp == null) { put(fetcher) to Source.ORIGIN } else { val isExpired = hasExpired(persistedTimestamp, timeLimit) @@ -82,39 +79,44 @@ class ExpirableCache(private val cache: Cache) : Fuse.Cacheable by c } } - @OptIn(ExperimentalTime::class) private fun hasExpired(persistedTimestamp: Long, timeLimit: Duration): Boolean { val now = System.currentTimeMillis() - val durationSincePersisted = Duration.milliseconds((now - persistedTimestamp)) + val durationSincePersisted = (now - persistedTimestamp).milliseconds return durationSincePersisted > timeLimit } } // region Value -@OptIn(ExperimentalTime::class) fun ExpirableCache.get( key: String, - getValue: (() -> T?)? = null, + getValue: (() -> T), timeLimit: Duration = Duration.INFINITE, useEntryEvenIfExpired: Boolean = false ): Result { - val fetcher = if (getValue == null) NoFetcher(key) else SimpleFetcher(key, getValue) + val fetcher = SimpleFetcher(key, getValue) return get(fetcher, timeLimit, useEntryEvenIfExpired) } -@OptIn(ExperimentalTime::class) +fun ExpirableCache.get( + key: String, + timeLimit: Duration = Duration.INFINITE, + useEntryEvenIfExpired: Boolean = false +): Result = get(NeverFetcher(key), timeLimit, useEntryEvenIfExpired) + fun ExpirableCache.getWithSource( key: String, - getValue: (() -> T?)? = null, + getValue: (() -> T), timeLimit: Duration = Duration.INFINITE, useEntryEvenIfExpired: Boolean = false ): Pair, Source> { - val fetcher = if (getValue == null) NoFetcher(key) else SimpleFetcher(key, getValue) + val fetcher = SimpleFetcher(key, getValue) return getWithSource(fetcher, timeLimit, useEntryEvenIfExpired) } -fun ExpirableCache.put(key: String, putValue: T? = null): Result { - val fetcher = if (putValue == null) NoFetcher(key) else SimpleFetcher(key, { putValue }) - return put(fetcher) -} +fun ExpirableCache.getWithSource( + key: String, + timeLimit: Duration = Duration.INFINITE, + useEntryEvenIfExpired: Boolean = false +): Pair, Source> = getWithSource(NeverFetcher(key), timeLimit, useEntryEvenIfExpired) + // endregion diff --git a/fuse/src/main/java/com/github/kittinunf/fuse/util/MD5.kt b/fuse/src/main/java/com/github/kittinunf/fuse/util/MD5.kt index fc315b4..b42b9ba 100644 --- a/fuse/src/main/java/com/github/kittinunf/fuse/util/MD5.kt +++ b/fuse/src/main/java/com/github/kittinunf/fuse/util/MD5.kt @@ -2,7 +2,7 @@ package com.github.kittinunf.fuse.util import java.security.MessageDigest -fun String.md5(): String { +internal fun String.md5(): String { val md = MessageDigest.getInstance("MD5") val digested = md.digest(toByteArray()) return digested.joinToString("") { diff --git a/fuse/src/test/java/com/github/kittinunf/fuse/FuseByteCacheTest.kt b/fuse/src/test/java/com/github/kittinunf/fuse/FuseByteCacheTest.kt index 826c9ee..adcd4d9 100644 --- a/fuse/src/test/java/com/github/kittinunf/fuse/FuseByteCacheTest.kt +++ b/fuse/src/test/java/com/github/kittinunf/fuse/FuseByteCacheTest.kt @@ -170,11 +170,12 @@ class FuseByteCacheTest : BaseTestCase() { val timestamp = cache.getTimestamp("timestamp") + assertThat(timestamp, notNullValue()) assertThat(timestamp, not(equalTo(-1L))) val timeLimit = 2000L assertThat( - System.currentTimeMillis() - timestamp, + System.currentTimeMillis() - timestamp!!, object : BaseMatcher() { override fun describeTo(description: Description) { description.appendText("$timestamp is over than $timeLimit") diff --git a/fuse/src/test/java/com/github/kittinunf/fuse/FuseScenarioTest.kt b/fuse/src/test/java/com/github/kittinunf/fuse/FuseExpirableScenarioTest.kt similarity index 87% rename from fuse/src/test/java/com/github/kittinunf/fuse/FuseScenarioTest.kt rename to fuse/src/test/java/com/github/kittinunf/fuse/FuseExpirableScenarioTest.kt index 2376f9d..5d51ec6 100644 --- a/fuse/src/test/java/com/github/kittinunf/fuse/FuseScenarioTest.kt +++ b/fuse/src/test/java/com/github/kittinunf/fuse/FuseExpirableScenarioTest.kt @@ -5,10 +5,10 @@ import com.github.kittinunf.fuse.core.Source import com.github.kittinunf.fuse.core.StringDataConvertible import com.github.kittinunf.fuse.core.build import com.github.kittinunf.fuse.core.fetch.Fetcher +import com.github.kittinunf.fuse.core.put import com.github.kittinunf.fuse.core.scenario.ExpirableCache import com.github.kittinunf.fuse.core.scenario.get import com.github.kittinunf.fuse.core.scenario.getWithSource -import com.github.kittinunf.fuse.core.scenario.put import com.github.kittinunf.result.Result import org.hamcrest.CoreMatchers.equalTo import org.hamcrest.CoreMatchers.not @@ -19,13 +19,11 @@ import org.junit.Before import org.junit.Test import java.io.File import kotlin.time.Duration -import kotlin.time.ExperimentalTime -import kotlin.time.milliseconds -import kotlin.time.seconds +import kotlin.time.Duration.Companion.milliseconds +import kotlin.time.Duration.Companion.seconds -class FuseScenarioTest : BaseTestCase() { +class FuseExpirableScenarioTest : BaseTestCase() { - @ExperimentalTime companion object { private val tempDir = createTempDir().absolutePath private val cache = CacheBuilder.config(tempDir, StringDataConvertible()).build() @@ -42,7 +40,6 @@ class FuseScenarioTest : BaseTestCase() { } } - @ExperimentalTime @Test fun fetchWhenNoData() { // remove first if it exists @@ -60,7 +57,33 @@ class FuseScenarioTest : BaseTestCase() { assertThat(timestamp, not(equalTo(-1L))) } - @ExperimentalTime + @Test + fun noFetchWhenNoData() { + // remove first if it exists + expirableCache.remove("hello", Source.MEM) + expirableCache.remove("hello", Source.DISK) + + val (value, error) = expirableCache.get("hello") + + assertThat(value, nullValue()) + assertThat(error, notNullValue()) + assertThat(error!!.message, equalTo("Value with key: hello is not found in cache")) + } + + @Test + fun noFetchWithSourceWithNoData() { + // remove first if it exists + expirableCache.remove("hello", Source.MEM) + expirableCache.remove("hello", Source.DISK) + + val (result, source) = expirableCache.getWithSource("hello") + val (value, error) = result + + assertThat(value, nullValue()) + assertThat(error, notNullValue()) + assertThat(error!!.message, equalTo("Value with key: hello is not found in cache")) + } + @Test fun fetchWithTimeLimitExpired() { val (value, error) = expirableCache.get("hello", { "world" }) @@ -82,7 +105,6 @@ class FuseScenarioTest : BaseTestCase() { assertThat(anotherError, nullValue()) } - @ExperimentalTime @Test fun fetchWithTimeLimitExpiredWithSource() { val (value, error) = expirableCache.get("hello-source", { "world" }) @@ -106,7 +128,6 @@ class FuseScenarioTest : BaseTestCase() { assertThat(anotherSource, equalTo(Source.ORIGIN)) } - @ExperimentalTime @Test fun fetchWithTimeLimitExpiredButStillForceToUse() { val (value, error) = expirableCache.get("expired", { "world" }) @@ -131,7 +152,6 @@ class FuseScenarioTest : BaseTestCase() { assertThat(anotherSource, equalTo(Source.MEM)) } - @ExperimentalTime @Test fun fetchWithTimeLimitNotExpired() { val (value, error) = expirableCache.get("not expired", { "world" }) @@ -151,7 +171,6 @@ class FuseScenarioTest : BaseTestCase() { assertThat(anotherSource, not(equalTo(Source.ORIGIN))) } - @ExperimentalTime @Test fun fetchWithTimeLimitNotExpiredButNotInMemory() { val (value, error) = expirableCache.get("not expired", { "world" }) @@ -172,7 +191,6 @@ class FuseScenarioTest : BaseTestCase() { assertThat(anotherSource, equalTo(Source.DISK)) } - @ExperimentalTime @Test fun fetchWithFetcherThatWillSuccess() { val goodFetcher = object : Fetcher { @@ -187,7 +205,6 @@ class FuseScenarioTest : BaseTestCase() { assertThat(error, nullValue()) } - @ExperimentalTime @Test fun fetchWithFetcherThatWillFail() { val (value, error) = expirableCache.get("can_fail", { "world" }) @@ -198,7 +215,7 @@ class FuseScenarioTest : BaseTestCase() { val failFetcher = object : Fetcher { override val key: String = "can_fail" - override fun fetch(): Result = Result.error(Exception("fail catcher")) + override fun fetch(): Result = Result.failure(Exception("fail catcher")) } // this will always force to be expired @@ -210,7 +227,6 @@ class FuseScenarioTest : BaseTestCase() { assertThat(anotherSource, not(equalTo(Source.ORIGIN))) } - @ExperimentalTime @Test fun putWillRetrieveDataFromTheFetcher() { val (value, error) = expirableCache.put("foofoo", "foofoo2") @@ -234,13 +250,12 @@ class FuseScenarioTest : BaseTestCase() { assertThat(anotherError, nullValue()) } - @ExperimentalTime @Test fun fetchWithFailureFetcherAndAlwaysExpiringEntry() { val failFetcher = object : Fetcher { override val key: String = "will_fail" - override fun fetch(): Result = Result.error(Exception("fail catcher")) + override fun fetch(): Result = Result.failure(Exception("fail catcher")) } val (result, source) = expirableCache.getWithSource(failFetcher, Duration.ZERO) @@ -251,7 +266,6 @@ class FuseScenarioTest : BaseTestCase() { assertThat(source, equalTo(Source.ORIGIN)) } - @ExperimentalTime @Test fun cleanEmptyCacheWillNotCrash() { val (value, error) = expirableCache.put("foofoo", "foofoo2") diff --git a/fuse/src/test/java/com/github/kittinunf/fuse/NetworkFetcher.kt b/fuse/src/test/java/com/github/kittinunf/fuse/NetworkFetcher.kt index a51148d..eb83aff 100644 --- a/fuse/src/test/java/com/github/kittinunf/fuse/NetworkFetcher.kt +++ b/fuse/src/test/java/com/github/kittinunf/fuse/NetworkFetcher.kt @@ -32,7 +32,7 @@ class NetworkFetcher( input.use { it.readBytes() } } - if (cancelled) return Result.error(RuntimeException("Fetch got cancelled")) + if (cancelled) return Result.failure(RuntimeException("Fetch got cancelled")) return connectResult.map { convertFromData(it) } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1a8d8f1..7337078 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,18 +1,17 @@ [versions] -kotlin = "1.6.10" -androidGradle = "7.0.4" -kotlinxSerialization = "1.3.1" +kotlin = "1.6.20" +androidGradle = "7.1.2" +kotlinxSerialization = "1.3.2" diskCache = "2.0.2" -result = "3.1.0" +result = "5.2.1" appCompat = "1.3.1" constraintLayout = "2.0.4" -fuel = "2.2.1" jacoco = "0.8.7" junit = "4.13.1" robolectric = "4.4" -minSdk = "21" +minSdk = "24" targetSdk = "30" [libraries] @@ -25,11 +24,9 @@ kotlin-test-junit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerialization" } diskCache = { module = "com.jakewharton:disklrucache", version.ref = "diskCache" } -result = { module = "com.github.kittinunf.result:result", version.ref = "result" } +result = { module = "com.github.kittinunf.result:result-jvm", version.ref = "result" } appCompat = { module = "androidx.appcompat:appcompat", version.ref = "appCompat" } constraintLayout = { module = "androidx.constraintlayout:constraintlayout", version.ref = "constraintLayout" } -fuel = { module = "com.github.kittinunf.fuel:fuel", version.ref = "fuel" } - test-junit = { module = "junit:junit", version.ref = "junit" } test-robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 2e6e589..41dfb87 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/sample/build.gradle.kts b/sample/build.gradle.kts index 99d3d6d..53703e8 100644 --- a/sample/build.gradle.kts +++ b/sample/build.gradle.kts @@ -4,7 +4,6 @@ plugins { } repositories { - jcenter() mavenCentral() google() } @@ -47,7 +46,6 @@ dependencies { implementation(libs.appCompat) implementation(libs.constraintLayout) - implementation(libs.fuel) implementation(project(":fuse")) implementation(project(":fuse-android")) } diff --git a/sample/src/main/java/com/github/kittinunf/fuse/sample/LocalTime.kt b/sample/src/main/java/com/github/kittinunf/fuse/sample/LocalTime.kt index f4dc230..dfc30d2 100644 --- a/sample/src/main/java/com/github/kittinunf/fuse/sample/LocalTime.kt +++ b/sample/src/main/java/com/github/kittinunf/fuse/sample/LocalTime.kt @@ -1,6 +1,6 @@ package com.github.kittinunf.fuse.sample -import com.github.kittinunf.fuel.core.ResponseDeserializable +import com.github.kittinunf.fuse.core.Fuse import org.json.JSONObject data class LocalTime( @@ -22,10 +22,21 @@ data class LocalTime( } } - val deserializer = object : ResponseDeserializable { - override fun deserialize(content: String): LocalTime? { - return fromJson(content) + val deserializer = object : Fuse.DataConvertible { + override fun convertFromData(bytes: ByteArray): LocalTime { + return fromJson(String(bytes)) + } + + override fun convertToData(value: LocalTime): ByteArray { + return value.toJson().toString(2).toByteArray() } } } + + fun toJson() = JSONObject().apply { + put("utc_datetime", utcDateTime) + put("timezone", timezone) + put("datetime", dateTime) + put("abbreviation", abbrev) + } } diff --git a/sample/src/main/java/com/github/kittinunf/fuse/sample/LocalTimeRepository.kt b/sample/src/main/java/com/github/kittinunf/fuse/sample/LocalTimeRepository.kt index 4025a6c..c5a2837 100644 --- a/sample/src/main/java/com/github/kittinunf/fuse/sample/LocalTimeRepository.kt +++ b/sample/src/main/java/com/github/kittinunf/fuse/sample/LocalTimeRepository.kt @@ -1,76 +1,80 @@ package com.github.kittinunf.fuse.sample -import android.content.Context -import com.github.kittinunf.fuel.core.FuelManager -import com.github.kittinunf.fuse.android.config -import com.github.kittinunf.fuse.android.defaultAndroidMemoryCache -import com.github.kittinunf.fuse.core.CacheBuilder import com.github.kittinunf.fuse.core.Source -import com.github.kittinunf.fuse.core.StringDataConvertible -import com.github.kittinunf.fuse.core.build -import com.github.kittinunf.fuse.core.fetch.Fetcher -import com.github.kittinunf.fuse.core.scenario.ExpirableCache import com.github.kittinunf.result.Result -import com.github.kittinunf.result.map +import java.util.concurrent.Executors +import java.util.concurrent.Future import kotlin.time.Duration -import kotlin.time.ExperimentalTime interface LocalTimeRepository { - fun getLocalTime(place: String): Result -} + fun getFromNetwork(location: String, handler: (Result) -> Unit) -interface CacheableLocalTimeRepository : LocalTimeRepository { + fun getFromCache(location: String, handler: (Result) -> Unit) - fun getLocalTimeIfNotExpired(place: String): Pair, Source> -} + // cache + network + fun getFromCacheThenNetwork(location: String, handler: (Result) -> Unit) + + fun getFromCacheIfNotExpired(location: String, duration: Duration, handler: (Result, Source) -> Unit) -private val fuel = FuelManager().apply { - basePath = "http://worldtimeapi.org/api/" + fun evictCache() } -class NetworkRepository : LocalTimeRepository { +class LocalTimeRepositoryImpl(private val network: LocalTimeService, private val cache: CacheableLocalTimeService) : LocalTimeRepository { - override fun getLocalTime(place: String): Result { - val area = place.continent - val location = place.area - return fuel.get("/timezone/$area/$location").responseObject(LocalTime.deserializer).third + override fun getFromNetwork(location: String, handler: (Result) -> Unit) { + dispatchDefault { + val result = network.getTime(location) + + mainThread { + handler(result) + } + } } -} -// in real-world application you should not do this, you should do some injection to construct this or something else, but this is a sample application so ¯\_(ツ)_/¯ -class CacheRepository(context: Context) : CacheableLocalTimeRepository { + override fun getFromCache(location: String, handler: (Result) -> Unit) { + dispatchDefault { + val result = cache.getLocalTimeOnlyCache(location) - private val cache = CacheBuilder.config(context, convertible = StringDataConvertible()) { - memCache = defaultAndroidMemoryCache() - }.build() + mainThread { + handler(result) + } + } + } - private val expirableCache = ExpirableCache(cache) + override fun getFromCacheThenNetwork(location: String, handler: (Result) -> Unit) { + dispatchDefault { + val r1 = cache.getTime(location) - override fun getLocalTime(place: String): Result { - val area = place.continent - val location = place.area - return cache.get(TimeFetcher(area, location)).map { LocalTime.fromJson(it) } - } + mainThread { + handler(r1) + } + } - @OptIn(ExperimentalTime::class) - override fun getLocalTimeIfNotExpired(place: String): Pair, Source> { - val area = place.continent - val location = place.area + dispatchDefault { + Thread.sleep(500) // deliberately make it slower + val r2 = network.getTime(location) - val result = expirableCache.getWithSource(TimeFetcher(area, location), timeLimit = Duration.minutes(5)) - return (result.first.map { LocalTime.fromJson(it) } to result.second) + mainThread { + handler(r2) + } + } } - private class TimeFetcher(private val area: String, private val location: String) : Fetcher { - override val key: String = LocalTime::class.java.name + override fun getFromCacheIfNotExpired(location: String, duration: Duration, handler: (Result, Source) -> Unit) { + dispatchDefault { + val (result, source) = cache.getLocalTimeIfNotExpired(location, duration) - override fun fetch(): Result = fuel.get("/timezone/$area/$location").responseString().third + mainThread { + handler(result, source) + } + } } -} -private val String.continent: String - get() = substringBefore("/") + override fun evictCache() { + cache.evictCache() + } +} -private val String.area: String - get() = substringAfter("/") +private fun T.dispatchDefault(block: FuseAsync.() -> Unit): Future = + dispatch(Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors()), block) diff --git a/sample/src/main/java/com/github/kittinunf/fuse/sample/LocalTimeService.kt b/sample/src/main/java/com/github/kittinunf/fuse/sample/LocalTimeService.kt index 58e8941..e3c5ddd 100644 --- a/sample/src/main/java/com/github/kittinunf/fuse/sample/LocalTimeService.kt +++ b/sample/src/main/java/com/github/kittinunf/fuse/sample/LocalTimeService.kt @@ -1,79 +1,81 @@ package com.github.kittinunf.fuse.sample +import android.content.Context +import com.github.kittinunf.fuse.android.config +import com.github.kittinunf.fuse.android.defaultAndroidMemoryCache +import com.github.kittinunf.fuse.core.CacheBuilder import com.github.kittinunf.fuse.core.Source +import com.github.kittinunf.fuse.core.build +import com.github.kittinunf.fuse.core.get +import com.github.kittinunf.fuse.core.scenario.ExpirableCache import com.github.kittinunf.result.Result -import java.util.concurrent.Executors -import java.util.concurrent.Future +import java.net.URL +import kotlin.time.Duration interface LocalTimeService { - fun getFromNetwork(location: String, handler: (Result) -> Unit) + fun getTime(place: String): Result +} - fun getFromCache(location: String, handler: (Result) -> Unit) +interface CacheableLocalTimeService : LocalTimeService { - // cache + network - fun getFromBoth(location: String, handler: (Result) -> Unit) + fun getLocalTimeOnlyCache(place: String): Result - // get from cache if available within 5 minutes otherwise refresh from network - fun getFromCacheIfNotExpired(location: String, handler: (Result, Source) -> Unit) -} + fun getLocalTimeIfNotExpired(place: String, duration: Duration): Pair, Source> -class LocalTimeServiceImpl(private val network: LocalTimeRepository, private val cache: CacheableLocalTimeRepository) : - LocalTimeService { + fun evictCache() +} - override fun getFromNetwork(location: String, handler: (Result) -> Unit) { - dispatchDefault { - val result = network.getLocalTime(location) +class NetworkService : LocalTimeService { - mainThread { - handler(result) - } - } + override fun getTime(place: String): Result { + val area = place.continent + val location = place.area + val fetcher = NetworkFetcher(URL("http://worldtimeapi.org/api/timezone/$area/$location"), LocalTime.deserializer) + return fetcher.fetch() } +} - override fun getFromCache(location: String, handler: (Result) -> Unit) { - dispatchDefault { - val result = cache.getLocalTime(location) +// in real-world application you should not do this, you should do some injection to construct this or something else, but this is a sample application so ¯\_(ツ)_/¯ +class CacheService(context: Context) : CacheableLocalTimeService { - mainThread { - handler(result) - } - } - } + private val cache = CacheBuilder.config(context, convertible = LocalTime.deserializer) { + memCache = defaultAndroidMemoryCache() + }.build() - override fun getFromBoth(location: String, handler: (Result) -> Unit) { - // this relies on the fact that network will always be slower than cache, in the real world usage, this probably a bad idea ... + private val expirableCache = ExpirableCache(cache) - dispatchDefault { - val r1 = cache.getLocalTime(location) + override fun getLocalTimeOnlyCache(place: String): Result { + val key = getURL(place).toString() + // fetch from cache only + return cache.get(key) + } - mainThread { - handler(r1) - } - } + override fun getTime(place: String): Result { + val fetcher = NetworkFetcher(getURL(place), LocalTime.deserializer) + // fetch from cache if there, if not then use NetworkFetcher to get new content + return cache.get(fetcher) + } - dispatchDefault { - val r2 = network.getLocalTime(location) + override fun getLocalTimeIfNotExpired(place: String, duration: Duration): Pair, Source> { + val fetcher = NetworkFetcher(getURL(place), LocalTime.deserializer) + // fetch from cache with expiration (in duration) if there, if not then use NetworkFetcher to get new content + return expirableCache.getWithSource(fetcher, timeLimit = duration) + } - mainThread { - handler(r2) - } - } + override fun evictCache() { + cache.removeAll() } - override fun getFromCacheIfNotExpired( - location: String, - handler: (Result, Source) -> Unit - ) { - dispatchDefault { - val (result, source) = cache.getLocalTimeIfNotExpired(location) - - mainThread { - handler(result, source) - } - } + private fun getURL(place: String): URL { + val area = place.continent + val location = place.area + return URL("http://worldtimeapi.org/api/timezone/$area/$location") } } -private fun T.dispatchDefault(block: FuseAsync.() -> Unit): Future = - dispatch(Executors.newScheduledThreadPool(Runtime.getRuntime().availableProcessors()), block) +private val String.continent: String + get() = substringBefore("/") + +private val String.area: String + get() = substringAfter("/") diff --git a/sample/src/main/java/com/github/kittinunf/fuse/sample/NetworkFetcher.kt b/sample/src/main/java/com/github/kittinunf/fuse/sample/NetworkFetcher.kt new file mode 100644 index 0000000..7441c83 --- /dev/null +++ b/sample/src/main/java/com/github/kittinunf/fuse/sample/NetworkFetcher.kt @@ -0,0 +1,53 @@ +package com.github.kittinunf.fuse.sample + +import com.github.kittinunf.fuse.core.Fuse +import com.github.kittinunf.fuse.core.fetch.Fetcher +import com.github.kittinunf.result.Result +import com.github.kittinunf.result.map +import java.net.HttpURLConnection +import java.net.URL +import java.net.URLConnection +import javax.net.ssl.HttpsURLConnection + +class NetworkFetcher( + private val url: URL, + private val convertible: Fuse.DataConvertible +) : Fetcher, Fuse.DataConvertible by convertible { + + override val key: String = url.toString() + + private var cancelled: Boolean = false + + override fun fetch(): Result { + val connectResult = Result.of { + val conn = establishConnection(url).apply { + readTimeout = 15000 + connectTimeout = 15000 + } + conn.connect() + + val input = conn.inputStream + input.use { it.readBytes() } + } + + if (cancelled) return Result.failure(RuntimeException("Fetch got cancelled")) + + return connectResult.map { convertFromData(it) } + } + + override fun cancel() { + cancelled = true + } + + private fun establishConnection(url: URL): URLConnection { + return if (url.protocol == "https") { + val connection = url.openConnection() as HttpsURLConnection + connection.apply { + sslSocketFactory = HttpsURLConnection.getDefaultSSLSocketFactory() + hostnameVerifier = HttpsURLConnection.getDefaultHostnameVerifier() + } + } else { + url.openConnection() as HttpURLConnection + } + } +} diff --git a/sample/src/main/java/com/github/kittinunf/fuse/sample/view/MainActivity.kt b/sample/src/main/java/com/github/kittinunf/fuse/sample/view/MainActivity.kt index 89356ae..0d5dcc7 100644 --- a/sample/src/main/java/com/github/kittinunf/fuse/sample/view/MainActivity.kt +++ b/sample/src/main/java/com/github/kittinunf/fuse/sample/view/MainActivity.kt @@ -3,22 +3,23 @@ package com.github.kittinunf.fuse.sample.view import android.os.Bundle import android.view.View import androidx.appcompat.app.AppCompatActivity -import com.github.kittinunf.fuse.sample.CacheRepository +import com.github.kittinunf.fuse.sample.CacheService import com.github.kittinunf.fuse.sample.LocalTime -import com.github.kittinunf.fuse.sample.LocalTimeServiceImpl -import com.github.kittinunf.fuse.sample.NetworkRepository +import com.github.kittinunf.fuse.sample.LocalTimeRepositoryImpl +import com.github.kittinunf.fuse.sample.NetworkService import com.github.kittinunf.fuse.sample.R import com.github.kittinunf.fuse.sample.databinding.ActivityMainBinding import com.github.kittinunf.result.Result +import kotlin.time.Duration.Companion.minutes -class MainActivity : AppCompatActivity(R.layout.activity_main) { +class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private val service by lazy { - LocalTimeServiceImpl( - network = NetworkRepository(), - cache = CacheRepository(this) + LocalTimeRepositoryImpl( + network = NetworkService(), + cache = CacheService(this) ) } @@ -29,7 +30,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) { val view = binding.root setContentView(view) - val place = "Asia/Bangkok" + val place = "Asia/Tokyo" with(binding) { networkButton.setOnClickListener { @@ -51,20 +52,26 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) { bothButton.setOnClickListener { setLoading(true) var count = 0 - service.getFromBoth(place) { + service.getFromCacheThenNetwork(place) { count++ updateTitle(count.toString()) updateResult(it) } } - cacheIfNotExpired.setOnClickListener { + cacheIfNotExpiredButton.setOnClickListener { setLoading(true) - service.getFromCacheIfNotExpired(place) { result, source -> + service.getFromCacheIfNotExpired(place, 5.minutes) { result, source -> updateTitle(source.name) updateResult(result) } } + + evictCacheButton.setOnClickListener { + service.evictCache() + updateTitle() + updateResult(Result.failure(RuntimeException("Cache is evicted"))) + } } } @@ -73,8 +80,7 @@ class MainActivity : AppCompatActivity(R.layout.activity_main) { with(binding) { when (result) { is Result.Success -> { - resultText.text = - "Location: ${result.value.timezone}\nTime: ${result.value.dateTime}\nTZ: ${result.value.abbrev}" + resultText.text = "Location: ${result.value.timezone}\nTime: ${result.value.dateTime}\nTZ: ${result.value.abbrev}" } is Result.Failure -> { diff --git a/sample/src/main/res/drawable-v24/ic_launcher_foreground.xml b/sample/src/main/res/drawable/ic_launcher_foreground.xml similarity index 100% rename from sample/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to sample/src/main/res/drawable/ic_launcher_foreground.xml diff --git a/sample/src/main/res/layout/activity_main.xml b/sample/src/main/res/layout/activity_main.xml index 9805f16..90789dc 100644 --- a/sample/src/main/res/layout/activity_main.xml +++ b/sample/src/main/res/layout/activity_main.xml @@ -11,12 +11,13 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginStart="16dp" - android:layout_marginLeft="16dp" + android:layout_marginTop="8dp" android:layout_marginBottom="8dp" android:textAppearance="@style/TextAppearance.AppCompat.Body1" android:textSize="20sp" app:layout_constraintBottom_toTopOf="@+id/resultText" app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@+id/evictCacheButton" tools:text="Title" />