feat: cache manager and webapp manifest repository

This commit is contained in:
2026-08-16 16:23:48 +08:00
parent 0153ec38ae
commit f3405e003f
4 changed files with 138 additions and 0 deletions
+18
View File
@@ -0,0 +1,18 @@
package top.yeij.hearth.cache
import java.io.File
// 文件缓存:以 key 为文件名,内容以 UTF-8 纯文本落盘
// File cache: uses key as filename, content stored as UTF-8 plain text
class CacheManager(private val baseDir: File) {
init { baseDir.mkdirs() }
fun save(key: String, content: String) {
File(baseDir, key).writeText(content)
}
fun load(key: String): String? {
val f = File(baseDir, key)
return if (f.exists()) f.readText() else null
}
}
@@ -0,0 +1,51 @@
package top.yeij.hearth.webapp
import com.google.gson.Gson
import top.yeij.hearth.cache.CacheManager
// Web 应用清单条目
// Web app manifest entry
data class WebApp(val id: String, val name: String, val icon: String, val url: String)
// 网络抽象接口:Task 10 的 CardRepository 也会复用
// HTTP abstraction reused by Task 10 CardRepository
interface HttpClient {
fun get(url: String): String?
}
// 清单拉取仓库:拉取成功缓存 body 并解析;失败回退缓存;无缓存返回空列表
// Manifest repository: on success cache + parse; on failure fall back to cache; no cache -> empty list
class WebAppRepository(
private val http: HttpClient,
private val cache: CacheManager,
private val manifestUrl: String,
) {
private val gson = Gson()
fun fetchManifest(): List<WebApp> {
val body = http.get(manifestUrl)
if (body != null) {
cache.save(KEY, body)
return parse(body)
}
val cached = cache.load(KEY) ?: return emptyList()
return parse(cached)
}
private fun parse(body: String): List<WebApp> {
val root = gson.fromJson(body, Map::class.java)
val apps = root["apps"] as? List<*> ?: return emptyList()
return apps.mapNotNull { m ->
val map = m as? Map<*, *> ?: return@mapNotNull null
val id = map["id"] as? String ?: return@mapNotNull null
val name = map["name"] as? String ?: return@mapNotNull null
val icon = map["icon"] as? String ?: return@mapNotNull null
val url = map["url"] as? String ?: return@mapNotNull null
WebApp(id, name, icon, url)
}
}
companion object {
private const val KEY = "webapp-manifest"
}
}
@@ -0,0 +1,25 @@
package top.yeij.hearth.cache
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNull
import org.junit.Test
import java.io.File
class CacheManagerTest {
@Test
fun saveThenLoad_roundTrips() {
val dir = File(System.getProperty("java.io.tmpdir"), "hearth-cache-test")
val cm = CacheManager(dir)
cm.save("manifest", "{\"version\":1}")
assertEquals("{\"version\":1}", cm.load("manifest"))
dir.deleteRecursively()
}
@Test
fun load_missingKey_returnsNull() {
val dir = File(System.getProperty("java.io.tmpdir"), "hearth-cache-miss")
val cm = CacheManager(dir)
assertNull(cm.load("nonexistent"))
dir.deleteRecursively()
}
}
@@ -0,0 +1,44 @@
package top.yeij.hearth.webapp
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import top.yeij.hearth.cache.CacheManager
import java.io.File
class WebAppRepositoryTest {
@Test
fun fetchManifest_success_parsesApps() {
val http = object : HttpClient {
override fun get(url: String) = """{"version":1,"apps":[{"id":"a","name":"云音乐","icon":"x.svg","url":"http://x/a"}]}"""
}
val repo = WebAppRepository(http, CacheManager(File(System.getProperty("java.io.tmpdir"), "w1")), "http://fake")
val apps = repo.fetchManifest()
assertEquals(1, apps.size)
assertEquals("云音乐", apps[0].name)
}
@Test
fun fetchManifest_failure_usesCache() {
val dir = File(System.getProperty("java.io.tmpdir"), "w2")
val cm = CacheManager(dir)
// 预写缓存(模拟之前拉取成功留下的缓存)
// Pre-seed the cache (simulate a previously successful fetch)
cm.save("webapp-manifest", """{"version":1,"apps":[{"id":"b","name":"缓存","icon":"x","url":"u"}]}""")
// 网络失败
// Network failure
val http = object : HttpClient { override fun get(url: String): String? = null }
val repo = WebAppRepository(http, cm, "http://fake")
assertEquals("缓存", repo.fetchManifest()[0].name)
dir.deleteRecursively()
}
@Test
fun fetchManifest_failure_withoutCache_returnsEmpty() {
val dir = File(System.getProperty("java.io.tmpdir"), "w3")
val http = object : HttpClient { override fun get(url: String): String? = null }
val repo = WebAppRepository(http, CacheManager(dir), "http://fake")
assertTrue(repo.fetchManifest().isEmpty())
dir.deleteRecursively()
}
}