feat: cache manager and webapp manifest repository
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user