mirror of
https://github.com/gkd-kit/gkd.git
synced 2026-09-03 07:19:59 +08:00
refactor: centralize subscription editing
This commit is contained in:
279
app/src/main/kotlin/li/songe/gkd/data/SubscriptionEditor.kt
Normal file
279
app/src/main/kotlin/li/songe/gkd/data/SubscriptionEditor.kt
Normal file
@@ -0,0 +1,279 @@
|
||||
package li.songe.gkd.data
|
||||
|
||||
class SubscriptionEditor(subscription: RawSubscription) {
|
||||
private val subscriptionId = subscription.id
|
||||
private var current = subscription
|
||||
|
||||
fun update(transform: RawSubscription.() -> RawSubscription) {
|
||||
setCurrent(current.transform())
|
||||
}
|
||||
|
||||
fun updateApp(
|
||||
appId: String,
|
||||
transform: RawSubscription.RawApp.() -> RawSubscription.RawApp,
|
||||
): Boolean {
|
||||
val newApps = current.apps.updateFirstOrNull(
|
||||
predicate = { it.id == appId },
|
||||
transform = { it.transform() },
|
||||
) ?: return false
|
||||
if (newApps !== current.apps) {
|
||||
setCurrent(current.copy(apps = newApps))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun putApp(app: RawSubscription.RawApp): RawSubscription.RawApp? {
|
||||
val previous = current.apps.find { it.id == app.id }
|
||||
if (previous == null) {
|
||||
setCurrent(current.copy(apps = current.apps + app))
|
||||
} else {
|
||||
updateApp(app.id) { app }
|
||||
}
|
||||
return previous
|
||||
}
|
||||
|
||||
fun mergeApp(app: RawSubscription.RawApp): RawSubscription.RawApp {
|
||||
val currentApp = current.apps.find { it.id == app.id }
|
||||
if (currentApp == null) {
|
||||
requireGroupNamesAvailable(emptyList(), app.groups)
|
||||
return app.copy(
|
||||
groups = normalizeAppGroupKeys(emptyList(), app.groups),
|
||||
).also(::putApp)
|
||||
}
|
||||
return appendAppGroups(app, app.groups)
|
||||
}
|
||||
|
||||
fun appendAppGroups(
|
||||
targetApp: RawSubscription.RawApp,
|
||||
groups: List<RawSubscription.RawAppGroup>,
|
||||
): RawSubscription.RawApp {
|
||||
val app = current.apps.find { it.id == targetApp.id } ?: targetApp
|
||||
requireGroupNamesAvailable(app.groups, groups)
|
||||
val normalizedGroups = normalizeAppGroupKeys(app.groups, groups)
|
||||
return app.copy(groups = app.groups + normalizedGroups).also(::putApp)
|
||||
}
|
||||
|
||||
private fun removeApp(appId: String): RawSubscription.RawApp? {
|
||||
val app = current.apps.find { it.id == appId } ?: return null
|
||||
setCurrent(current.copy(apps = current.apps.filterNot { it.id == appId }))
|
||||
return app
|
||||
}
|
||||
|
||||
fun updateCategory(
|
||||
categoryKey: Int,
|
||||
transform: RawSubscription.RawCategory.() -> RawSubscription.RawCategory,
|
||||
): Boolean {
|
||||
val newCategories = current.categories.updateFirstOrNull(
|
||||
predicate = { it.key == categoryKey },
|
||||
transform = { it.transform() },
|
||||
) ?: return false
|
||||
if (newCategories !== current.categories) {
|
||||
setCurrent(current.copy(categories = newCategories))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun putCategory(category: RawSubscription.RawCategory): RawSubscription.RawCategory? {
|
||||
val previous = current.categories.find { it.key == category.key }
|
||||
if (previous == null) {
|
||||
setCurrent(current.copy(categories = current.categories + category))
|
||||
} else {
|
||||
updateCategory(category.key) { category }
|
||||
}
|
||||
return previous
|
||||
}
|
||||
|
||||
fun removeCategory(categoryKey: Int): RawSubscription.RawCategory? {
|
||||
val category = current.categories.find { it.key == categoryKey } ?: return null
|
||||
setCurrent(
|
||||
current.copy(categories = current.categories.filterNot { it.key == categoryKey }),
|
||||
)
|
||||
return category
|
||||
}
|
||||
|
||||
private fun updateGlobalGroup(
|
||||
groupKey: Int,
|
||||
transform: RawSubscription.RawGlobalGroup.() -> RawSubscription.RawGlobalGroup,
|
||||
): Boolean {
|
||||
val newGroups = current.globalGroups.updateFirstOrNull(
|
||||
predicate = { it.key == groupKey },
|
||||
transform = { it.transform() },
|
||||
) ?: return false
|
||||
if (newGroups !== current.globalGroups) {
|
||||
setCurrent(current.copy(globalGroups = newGroups))
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
private fun putGlobalGroup(
|
||||
group: RawSubscription.RawGlobalGroup,
|
||||
): RawSubscription.RawGlobalGroup? {
|
||||
val previous = current.globalGroups.find { it.key == group.key }
|
||||
if (previous == null) {
|
||||
setCurrent(current.copy(globalGroups = current.globalGroups + group))
|
||||
} else {
|
||||
updateGlobalGroup(group.key) { group }
|
||||
}
|
||||
return previous
|
||||
}
|
||||
|
||||
fun appendGlobalGroup(
|
||||
group: RawSubscription.RawGlobalGroup,
|
||||
): RawSubscription.RawGlobalGroup {
|
||||
requireGroupNamesAvailable(current.globalGroups, listOf(group))
|
||||
val normalizedGroup = normalizeGlobalGroupKey(current.globalGroups, group)
|
||||
putGlobalGroup(normalizedGroup)
|
||||
return normalizedGroup
|
||||
}
|
||||
|
||||
fun removeGlobalGroups(
|
||||
predicate: (RawSubscription.RawGlobalGroup) -> Boolean,
|
||||
): List<RawSubscription.RawGlobalGroup> {
|
||||
val (removed, remaining) = current.globalGroups.partition(predicate)
|
||||
if (removed.isNotEmpty()) {
|
||||
setCurrent(current.copy(globalGroups = remaining))
|
||||
}
|
||||
return removed
|
||||
}
|
||||
|
||||
private fun updateAppGroup(
|
||||
appId: String,
|
||||
groupKey: Int,
|
||||
transform: RawSubscription.RawAppGroup.() -> RawSubscription.RawAppGroup,
|
||||
): Boolean {
|
||||
var groupFound = false
|
||||
updateApp(appId) {
|
||||
val newGroups = groups.updateFirstOrNull(
|
||||
predicate = { it.key == groupKey },
|
||||
transform = { group -> group.transform() },
|
||||
) ?: return@updateApp this
|
||||
groupFound = true
|
||||
if (newGroups === groups) {
|
||||
this
|
||||
} else {
|
||||
copy(groups = newGroups)
|
||||
}
|
||||
}
|
||||
return groupFound
|
||||
}
|
||||
|
||||
fun replaceAppGroup(
|
||||
targetApp: RawSubscription.RawApp,
|
||||
groupKey: Int,
|
||||
expectedGroup: RawSubscription.RawAppGroup,
|
||||
newGroup: RawSubscription.RawAppGroup,
|
||||
) {
|
||||
require(newGroup.key == groupKey) { "规则key与当前规则不一致" }
|
||||
val currentApp = current.apps.find { it.id == targetApp.id }
|
||||
if (currentApp == null) {
|
||||
putApp(targetApp.copy(groups = listOf(newGroup)))
|
||||
return
|
||||
}
|
||||
val updated = updateAppGroup(targetApp.id, groupKey) {
|
||||
if (this != expectedGroup) error("规则已发生变化,请重新编辑")
|
||||
requireGroupNamesAvailable(
|
||||
currentApp.groups.filterNot { it.key == groupKey },
|
||||
listOf(newGroup),
|
||||
)
|
||||
newGroup
|
||||
}
|
||||
if (!updated) error("规则已不存在")
|
||||
}
|
||||
|
||||
fun replaceGlobalGroup(
|
||||
groupKey: Int,
|
||||
expectedGroup: RawSubscription.RawGlobalGroup,
|
||||
newGroup: RawSubscription.RawGlobalGroup,
|
||||
) {
|
||||
require(newGroup.key == groupKey) { "规则key与当前规则不一致" }
|
||||
val updated = updateGlobalGroup(groupKey) {
|
||||
if (this != expectedGroup) error("规则已发生变化,请重新编辑")
|
||||
requireGroupNamesAvailable(
|
||||
current.globalGroups.filterNot { it.key == groupKey },
|
||||
listOf(newGroup),
|
||||
)
|
||||
newGroup
|
||||
}
|
||||
if (!updated) error("规则已不存在")
|
||||
}
|
||||
|
||||
fun removeAppGroups(
|
||||
appId: String,
|
||||
removeAppIfEmpty: Boolean = false,
|
||||
predicate: (RawSubscription.RawAppGroup) -> Boolean,
|
||||
): List<RawSubscription.RawAppGroup> {
|
||||
val app = current.apps.find { it.id == appId } ?: return emptyList()
|
||||
val (removed, remaining) = app.groups.partition(predicate)
|
||||
if (removed.isEmpty()) return emptyList()
|
||||
if (removeAppIfEmpty && remaining.isEmpty()) {
|
||||
removeApp(appId)
|
||||
} else {
|
||||
updateApp(appId) { copy(groups = remaining) }
|
||||
}
|
||||
return removed
|
||||
}
|
||||
|
||||
fun build(): RawSubscription = current
|
||||
|
||||
private fun setCurrent(subscription: RawSubscription) {
|
||||
require(subscription.id == subscriptionId) {
|
||||
"订阅id不可修改: $subscriptionId -> ${subscription.id}"
|
||||
}
|
||||
current = subscription
|
||||
}
|
||||
|
||||
private fun requireGroupNamesAvailable(
|
||||
existingGroups: List<RawSubscription.RawGroupProps>,
|
||||
newGroups: List<RawSubscription.RawGroupProps>,
|
||||
) {
|
||||
val usedNames = existingGroups.mapTo(mutableSetOf()) { it.name }
|
||||
newGroups.forEach { group ->
|
||||
if (!usedNames.add(group.name)) {
|
||||
error("已存在同名「${group.name}」规则")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeAppGroupKeys(
|
||||
existingGroups: List<RawSubscription.RawAppGroup>,
|
||||
newGroups: List<RawSubscription.RawAppGroup>,
|
||||
): List<RawSubscription.RawAppGroup> {
|
||||
val usedKeys = existingGroups.mapTo(mutableSetOf()) { it.key }
|
||||
return newGroups.map { group ->
|
||||
if (usedKeys.add(group.key)) {
|
||||
group
|
||||
} else {
|
||||
val newKey = requireNotNull(usedKeys.maxOrNull()) + 1
|
||||
usedKeys.add(newKey)
|
||||
group.copy(key = newKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun normalizeGlobalGroupKey(
|
||||
existingGroups: List<RawSubscription.RawGlobalGroup>,
|
||||
group: RawSubscription.RawGlobalGroup,
|
||||
): RawSubscription.RawGlobalGroup {
|
||||
if (existingGroups.none { it.key == group.key }) return group
|
||||
return group.copy(key = existingGroups.maxOf { it.key } + 1)
|
||||
}
|
||||
}
|
||||
|
||||
fun RawSubscription.edit(
|
||||
block: SubscriptionEditor.() -> Unit,
|
||||
): RawSubscription = SubscriptionEditor(this).apply(block).build()
|
||||
|
||||
private inline fun <T> List<T>.updateFirstOrNull(
|
||||
predicate: (T) -> Boolean,
|
||||
transform: (T) -> T,
|
||||
): List<T>? {
|
||||
val index = indexOfFirst(predicate)
|
||||
if (index < 0) return null
|
||||
val current = this[index]
|
||||
val updated = transform(current)
|
||||
return if (updated == current) {
|
||||
this
|
||||
} else {
|
||||
toMutableList().apply { set(index, updated) }
|
||||
}
|
||||
}
|
||||
128
app/src/main/kotlin/li/songe/gkd/data/SubscriptionInputParser.kt
Normal file
128
app/src/main/kotlin/li/songe/gkd/data/SubscriptionInputParser.kt
Normal file
@@ -0,0 +1,128 @@
|
||||
package li.songe.gkd.data
|
||||
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import li.songe.json5.Json5
|
||||
|
||||
class SubscriptionInputParser private constructor(
|
||||
val jsonObject: JsonObject,
|
||||
) {
|
||||
fun parseApp(): RawSubscription.RawApp {
|
||||
val app = parseAppObject()
|
||||
app.groups.forEach(::requireValid)
|
||||
return app
|
||||
}
|
||||
|
||||
fun parseAppGroups(expectedAppId: String): List<RawSubscription.RawAppGroup> {
|
||||
val groups = if (isAppObject()) {
|
||||
parseAppObject(expectedAppId).groups
|
||||
} else {
|
||||
listOf(parseRule { RawSubscription.parseAppGroup(jsonObject) })
|
||||
}
|
||||
groups.forEach(::requireValid)
|
||||
return groups
|
||||
}
|
||||
|
||||
fun parseAppGroup(expectedAppId: String): RawSubscription.RawAppGroup {
|
||||
val group = if (isAppObject()) {
|
||||
parseAppObject(expectedAppId).groups.first()
|
||||
} else {
|
||||
parseRule { RawSubscription.parseAppGroup(jsonObject) }
|
||||
}
|
||||
requireValid(group)
|
||||
return group
|
||||
}
|
||||
|
||||
fun parseGlobalGroup(): RawSubscription.RawGlobalGroup {
|
||||
val group = parseRule { RawSubscription.parseGlobalGroup(jsonObject) }
|
||||
requireValid(group)
|
||||
return group
|
||||
}
|
||||
|
||||
private fun isAppObject(): Boolean = jsonObject["groups"] is JsonArray
|
||||
|
||||
private fun parseAppObject(
|
||||
expectedAppId: String? = null,
|
||||
): RawSubscription.RawApp = parseRule {
|
||||
if (expectedAppId != null) requireExpectedAppId(expectedAppId)
|
||||
RawSubscription.parseApp(jsonObject).requireGroups()
|
||||
}
|
||||
|
||||
private fun requireExpectedAppId(expectedAppId: String) {
|
||||
val id = jsonObject["id"] ?: error("缺少id")
|
||||
if (id !is JsonPrimitive || !id.isString || id.content != expectedAppId) {
|
||||
error("id与当前应用不一致")
|
||||
}
|
||||
}
|
||||
|
||||
private fun RawSubscription.RawApp.requireGroups(): RawSubscription.RawApp {
|
||||
if (groups.isEmpty()) error("至少输入一个规则")
|
||||
return this
|
||||
}
|
||||
|
||||
private fun <T : RawSubscription.RawGroupProps> requireValid(group: T): T {
|
||||
group.errorDesc?.let(::error)
|
||||
return group
|
||||
}
|
||||
|
||||
private fun <T> parseRule(block: () -> T): T = try {
|
||||
block()
|
||||
} catch (e: Exception) {
|
||||
error("非法规则\n${e.message}")
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun parse(
|
||||
source: String,
|
||||
defaultGroupKey: Int = 0,
|
||||
): SubscriptionInputParser {
|
||||
val element = try {
|
||||
Json5.parseToJson5Element(source)
|
||||
} catch (e: Exception) {
|
||||
error("非法格式\n${e.message}")
|
||||
}
|
||||
if (element !is JsonObject) error("规则应为对象格式")
|
||||
return SubscriptionInputParser(element.fillGroupKeys(defaultGroupKey))
|
||||
}
|
||||
|
||||
private fun JsonObject.fillGroupKeys(defaultGroupKey: Int): JsonObject {
|
||||
val groups = this["groups"]
|
||||
if (groups is JsonArray) {
|
||||
val usedKeys = groups.mapNotNull { group ->
|
||||
(group as? JsonObject)?.get("key")?.groupKeyOrNull()
|
||||
}.toMutableSet()
|
||||
var nextKey = defaultGroupKey
|
||||
var changed = false
|
||||
val newGroups = groups.map { group ->
|
||||
if (group is JsonObject && group["name"] != null && group["key"] == null) {
|
||||
while (nextKey in usedKeys) nextKey += 1
|
||||
changed = true
|
||||
JsonObject(group + ("key" to JsonPrimitive(nextKey))).also {
|
||||
usedKeys.add(nextKey)
|
||||
nextKey += 1
|
||||
}
|
||||
} else {
|
||||
group
|
||||
}
|
||||
}
|
||||
return if (changed) {
|
||||
JsonObject(this + ("groups" to JsonArray(newGroups)))
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
return if (this["name"] != null && this["key"] == null) {
|
||||
JsonObject(this + ("key" to JsonPrimitive(defaultGroupKey)))
|
||||
} else {
|
||||
this
|
||||
}
|
||||
}
|
||||
|
||||
private fun JsonElement.groupKeyOrNull(): Int? {
|
||||
val primitive = this as? JsonPrimitive ?: return null
|
||||
return primitive.content.toIntOrNull()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ import kotlinx.coroutines.withContext
|
||||
import li.songe.gkd.data.CategoryConfig
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
import li.songe.gkd.data.edit
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.ui.component.batchUpdateGroupEnable
|
||||
import li.songe.gkd.ui.component.toGroupState
|
||||
@@ -110,26 +111,11 @@ class SubsAppGroupListVm(val route: SubsAppGroupListRoute) : BaseViewModel() {
|
||||
suspend fun deleteSelectedGroups(selectedKeys: Set<Int>): Int {
|
||||
var deletedSize = 0
|
||||
subscription.update { current ->
|
||||
val app = current.apps.find { it.id == route.appId }
|
||||
?: return@update current
|
||||
val currentKeys = app.groups.mapTo(mutableSetOf()) { it.key }
|
||||
val keysToDelete = selectedKeys intersect currentKeys
|
||||
deletedSize = keysToDelete.size
|
||||
if (keysToDelete.isEmpty()) {
|
||||
current
|
||||
} else if (keysToDelete == currentKeys) {
|
||||
current.copy(
|
||||
apps = current.apps.filter { it.id != route.appId },
|
||||
)
|
||||
} else {
|
||||
current.copy(
|
||||
apps = current.apps.toMutableList().apply {
|
||||
set(
|
||||
indexOfFirst { it.id == route.appId },
|
||||
app.copy(groups = app.groups.filterNot { it.key in keysToDelete }),
|
||||
)
|
||||
},
|
||||
)
|
||||
current.edit {
|
||||
deletedSize = removeAppGroups(
|
||||
appId = route.appId,
|
||||
removeAppIfEmpty = true,
|
||||
) { it.key in selectedKeys }.size
|
||||
}
|
||||
}
|
||||
return deletedSize
|
||||
|
||||
@@ -9,6 +9,7 @@ import li.songe.gkd.MainViewModel
|
||||
import li.songe.gkd.data.CategoryConfig
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
import li.songe.gkd.data.edit
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.store.storeFlow
|
||||
import li.songe.gkd.ui.component.updateRuleGroupEnable
|
||||
@@ -181,14 +182,12 @@ class SubsCategoryGroupVm(
|
||||
if (category.name == name && (category.desc ?: "") == description) {
|
||||
current
|
||||
} else {
|
||||
current.copy(
|
||||
categories = current.categories.toMutableList().apply {
|
||||
set(
|
||||
indexOfFirst { it.key == category.key },
|
||||
category.copy(name = name, desc = description),
|
||||
)
|
||||
},
|
||||
)
|
||||
current.edit {
|
||||
val updated = updateCategory(category.key) {
|
||||
copy(name = name, desc = description)
|
||||
}
|
||||
if (!updated) error("类别已不存在")
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (changed) "更新成功" else "未修改"
|
||||
@@ -196,11 +195,7 @@ class SubsCategoryGroupVm(
|
||||
|
||||
suspend fun deleteCategory() {
|
||||
subscription.update { current ->
|
||||
current.copy(
|
||||
categories = current.categories.filterNot {
|
||||
it.key == route.categoryKey
|
||||
},
|
||||
)
|
||||
current.edit { removeCategory(route.categoryKey) }
|
||||
}
|
||||
DbSet.categoryConfigDao.deleteByCategoryKey(
|
||||
route.subsId,
|
||||
|
||||
@@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import li.songe.gkd.data.CategoryConfig
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.edit
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.ui.share.BaseViewModel
|
||||
import li.songe.gkd.ui.share.Loadable
|
||||
@@ -75,18 +76,16 @@ class SubsCategoryVm(val route: SubsCategoryRoute) : BaseViewModel() {
|
||||
if (current.categories.any { category -> category.name == name }) {
|
||||
error("不可添加同名类别")
|
||||
}
|
||||
current.copy(
|
||||
categories = current.categories.toMutableList().apply {
|
||||
add(
|
||||
RawSubscription.RawCategory(
|
||||
key = (current.categories.maxOfOrNull { it.key } ?: -1) + 1,
|
||||
enable = null,
|
||||
name = name,
|
||||
desc = description,
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
current.edit {
|
||||
putCategory(
|
||||
RawSubscription.RawCategory(
|
||||
key = (current.categories.maxOfOrNull { it.key } ?: -1) + 1,
|
||||
enable = null,
|
||||
name = name,
|
||||
desc = description,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
return "添加成功"
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
import li.songe.gkd.data.edit
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.ui.component.batchUpdateGroupEnable
|
||||
import li.songe.gkd.ui.component.toGroupState
|
||||
@@ -76,9 +77,7 @@ class SubsGlobalGroupListVm(val route: SubsGlobalGroupListRoute) : BaseViewModel
|
||||
|
||||
suspend fun deleteSelectedGroups(selectedKeys: Set<Int>) {
|
||||
subscription.update { current ->
|
||||
current.copy(globalGroups = current.globalGroups.filterNot {
|
||||
it.key in selectedKeys
|
||||
})
|
||||
current.edit { removeGlobalGroups { it.key in selectedKeys } }
|
||||
}
|
||||
DbSet.subsConfigDao.batchDeleteGlobalGroupConfig(route.subsItemId, selectedKeys.toList())
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ fun UpsertRuleGroupPage(route: UpsertRuleGroupRoute) {
|
||||
})
|
||||
|
||||
val onClickSave = throttle(vm.scope.launchAsFn(Dispatchers.Main) {
|
||||
withContext(Dispatchers.Default) { vm.saveRule() }
|
||||
val addedAppId = withContext(Dispatchers.Default) { vm.saveRule() }
|
||||
context.imeController.hideAndAwait()
|
||||
if (forward) {
|
||||
if (appId == null) {
|
||||
@@ -94,7 +94,7 @@ fun UpsertRuleGroupPage(route: UpsertRuleGroupRoute) {
|
||||
mainVm.navigatePage(
|
||||
SubsAppGroupListRoute(
|
||||
subsItemId = subsId,
|
||||
appId = vm.addAppId ?: appId,
|
||||
appId = addedAppId ?: appId,
|
||||
),
|
||||
replaced = true,
|
||||
)
|
||||
|
||||
@@ -3,15 +3,12 @@ package li.songe.gkd.ui
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.SubscriptionInputParser
|
||||
import li.songe.gkd.data.edit
|
||||
import li.songe.gkd.ui.share.BaseViewModel
|
||||
import li.songe.gkd.ui.style.clearJson5TransformationCache
|
||||
import li.songe.gkd.util.LogUtils
|
||||
import li.songe.gkd.util.toast
|
||||
import li.songe.json5.Json5
|
||||
|
||||
data class UpsertRuleGroupUiState(
|
||||
val initialGroup: RawSubscription.RawGroupProps?,
|
||||
@@ -71,13 +68,12 @@ class UpsertRuleGroupVm(val route: UpsertRuleGroupRoute) : BaseViewModel() {
|
||||
if (!isEdit) return !text.isBlank()
|
||||
if (state.initialText == text) return false
|
||||
return state.initialGroup?.cacheJsonObject !=
|
||||
runCatching { Json5.parseToJson5Element(text) }.getOrNull()
|
||||
runCatching {
|
||||
SubscriptionInputParser.parse(text, groupKey ?: 0).jsonObject
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
|
||||
var addAppId: String? = null
|
||||
|
||||
suspend fun saveRule() {
|
||||
suspend fun saveRule(): String? {
|
||||
val state = requireUiState()
|
||||
val initialGroup = state.initialGroup
|
||||
val text = textFlow.value ?: state.initialText
|
||||
@@ -86,221 +82,72 @@ class UpsertRuleGroupVm(val route: UpsertRuleGroupRoute) : BaseViewModel() {
|
||||
}
|
||||
if (text == state.initialText) {
|
||||
toast("规则无变动")
|
||||
return
|
||||
return null
|
||||
}
|
||||
var jsonObject = runCatching { Json5.parseToJson5Element(text) }.run {
|
||||
if (isFailure) {
|
||||
error("非法格式\n${exceptionOrNull()?.message}")
|
||||
}
|
||||
getOrThrow()
|
||||
}
|
||||
if (jsonObject !is JsonObject) {
|
||||
error("规则应为对象格式")
|
||||
}
|
||||
// 自动填充 key
|
||||
if (jsonObject["name"] != null && jsonObject["key"] == null) {
|
||||
jsonObject = JsonObject(jsonObject + mapOf("key" to JsonPrimitive(groupKey ?: 0)))
|
||||
}
|
||||
if (jsonObject["id"] is JsonPrimitive && jsonObject["groups"] is JsonArray) {
|
||||
val groups = jsonObject["groups"] as JsonArray
|
||||
val newGroups = groups.map {
|
||||
if (it is JsonObject && it["name"] != null && it["key"] == null) {
|
||||
JsonObject(it + mapOf("key" to JsonPrimitive(groupKey ?: 0)))
|
||||
} else {
|
||||
it
|
||||
}
|
||||
}
|
||||
jsonObject = JsonObject(mapOf("groups" to JsonArray(newGroups)) + jsonObject)
|
||||
}
|
||||
|
||||
if (jsonObject == initialGroup?.cacheJsonObject) {
|
||||
val input = SubscriptionInputParser.parse(text, groupKey ?: 0)
|
||||
if (input.jsonObject == initialGroup?.cacheJsonObject) {
|
||||
toast("规则无变动")
|
||||
return
|
||||
return null
|
||||
}
|
||||
var addedAppId: String? = null
|
||||
if (groupKey != null) {
|
||||
var newGroup = try {
|
||||
if (appId != null) {
|
||||
if (jsonObject["groups"] is JsonArray) {
|
||||
val id = jsonObject["id"] ?: error("缺少id")
|
||||
if (!(id is JsonPrimitive && id.isString && id.content == appId)) {
|
||||
error("id与当前应用不一致")
|
||||
}
|
||||
RawSubscription.parseApp(jsonObject).let { newApp ->
|
||||
if (newApp.groups.isEmpty()) {
|
||||
error("至少输入一个规则")
|
||||
}
|
||||
newApp.groups.first()
|
||||
}
|
||||
} else {
|
||||
null
|
||||
} ?: RawSubscription.parseAppGroup(jsonObject)
|
||||
} else {
|
||||
RawSubscription.parseGlobalGroup(jsonObject)
|
||||
if (appId != null) {
|
||||
val newGroup = input.parseAppGroup(appId).copy(key = groupKey)
|
||||
if (newGroup == initialGroup) {
|
||||
toast("规则无变动")
|
||||
return null
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.d(e)
|
||||
error("非法规则\n${e.message}")
|
||||
}
|
||||
newGroup.errorDesc?.let(::error)
|
||||
if (newGroup.key != groupKey) {
|
||||
// 自动修正 key 与原来一致
|
||||
newGroup = when (newGroup) {
|
||||
is RawSubscription.RawAppGroup -> newGroup.copy(key = groupKey)
|
||||
is RawSubscription.RawGlobalGroup -> newGroup.copy(key = groupKey)
|
||||
val originalGroup = requireNotNull(
|
||||
editBaseGroup ?: initialGroup,
|
||||
) as RawSubscription.RawAppGroup
|
||||
requiredSubscription.update { subscription ->
|
||||
subscription.edit {
|
||||
replaceAppGroup(
|
||||
targetApp = subscription.getApp(appId),
|
||||
groupKey = groupKey,
|
||||
expectedGroup = originalGroup,
|
||||
newGroup = newGroup,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (newGroup == initialGroup) {
|
||||
toast("规则无变动")
|
||||
return
|
||||
}
|
||||
val originalGroup = requireNotNull(editBaseGroup ?: initialGroup)
|
||||
requiredSubscription.update { subscription ->
|
||||
if (appId != null) {
|
||||
newGroup as RawSubscription.RawAppGroup
|
||||
val appIndex = subscription.apps.indexOfFirst { it.id == appId }
|
||||
if (appIndex < 0) error("应用不存在")
|
||||
val app = subscription.apps[appIndex]
|
||||
val groupIndex = app.groups.indexOfFirst { it.key == groupKey }
|
||||
if (groupIndex < 0) error("规则已不存在")
|
||||
if (app.groups[groupIndex] != originalGroup) {
|
||||
error("规则已发生变化,请重新编辑")
|
||||
} else {
|
||||
val newGroup = input.parseGlobalGroup().copy(key = groupKey)
|
||||
if (newGroup == initialGroup) {
|
||||
toast("规则无变动")
|
||||
return null
|
||||
}
|
||||
val originalGroup = requireNotNull(
|
||||
editBaseGroup ?: initialGroup,
|
||||
) as RawSubscription.RawGlobalGroup
|
||||
requiredSubscription.update { subscription ->
|
||||
subscription.edit {
|
||||
replaceGlobalGroup(groupKey, originalGroup, newGroup)
|
||||
}
|
||||
subscription.copy(
|
||||
apps = subscription.apps.toMutableList().apply {
|
||||
set(
|
||||
appIndex,
|
||||
app.copy(
|
||||
groups = app.groups.toMutableList().apply {
|
||||
set(groupIndex, newGroup)
|
||||
},
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
} else {
|
||||
newGroup as RawSubscription.RawGlobalGroup
|
||||
val groupIndex = subscription.globalGroups.indexOfFirst {
|
||||
it.key == groupKey
|
||||
}
|
||||
if (groupIndex < 0) error("规则已不存在")
|
||||
if (subscription.globalGroups[groupIndex] != originalGroup) {
|
||||
error("规则已发生变化,请重新编辑")
|
||||
}
|
||||
subscription.copy(
|
||||
globalGroups = subscription.globalGroups.toMutableList().apply {
|
||||
set(groupIndex, newGroup)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (isAddAnyApp) {
|
||||
val newApp = try {
|
||||
RawSubscription.parseApp(jsonObject).apply {
|
||||
if (groups.isEmpty()) {
|
||||
error("至少输入一个规则")
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
LogUtils.d(e)
|
||||
error("非法规则\n${e.message}")
|
||||
}
|
||||
val newApp = input.parseApp()
|
||||
requiredSubscription.update { subscription ->
|
||||
val appIndex = subscription.apps.indexOfFirst { it.id == newApp.id }
|
||||
if (appIndex < 0) {
|
||||
subscription.copy(apps = subscription.apps + newApp)
|
||||
} else {
|
||||
val oldApp = subscription.apps[appIndex]
|
||||
newApp.groups.forEach { group ->
|
||||
checkGroupKeyName(oldApp.groups, group)
|
||||
}
|
||||
val usedKeys = oldApp.groups.mapTo(mutableSetOf()) { it.key }
|
||||
val newGroups = newApp.groups.map { group ->
|
||||
if (group.key in usedKeys) {
|
||||
val newKey = requireNotNull(usedKeys.maxOrNull()) + 1
|
||||
group.copy(key = newKey).also { usedKeys.add(newKey) }
|
||||
} else {
|
||||
group.also { usedKeys.add(it.key) }
|
||||
}
|
||||
}
|
||||
subscription.copy(
|
||||
apps = subscription.apps.toMutableList().apply {
|
||||
set(appIndex, oldApp.copy(groups = oldApp.groups + newGroups))
|
||||
},
|
||||
)
|
||||
}
|
||||
subscription.edit { mergeApp(newApp) }
|
||||
}
|
||||
addAppId = newApp.id
|
||||
addedAppId = newApp.id
|
||||
} else if (appId != null) {
|
||||
// add specified app group
|
||||
val newGroups = try {
|
||||
if (jsonObject["groups"] is JsonArray) {
|
||||
val id = jsonObject["id"] ?: error("缺少id")
|
||||
if (!(id is JsonPrimitive && id.isString && id.content == appId)) {
|
||||
error("id与当前应用不一致")
|
||||
}
|
||||
RawSubscription.parseApp(jsonObject).apply {
|
||||
if (groups.isEmpty()) {
|
||||
error("至少输入一个规则")
|
||||
}
|
||||
}.groups
|
||||
} else {
|
||||
null
|
||||
} ?: listOf(RawSubscription.parseAppGroup(jsonObject))
|
||||
} catch (e: Exception) {
|
||||
LogUtils.d(e)
|
||||
error("非法规则\n${e.message}")
|
||||
}
|
||||
newGroups.forEach { g ->
|
||||
g.errorDesc?.let { error(it) }
|
||||
}
|
||||
val newGroups = input.parseAppGroups(appId)
|
||||
requiredSubscription.update { subscription ->
|
||||
val appIndex = subscription.apps.indexOfFirst { it.id == appId }
|
||||
if (appIndex < 0) error("应用不存在")
|
||||
val oldApp = subscription.apps[appIndex]
|
||||
newGroups.forEach { group ->
|
||||
checkGroupKeyName(oldApp.groups, group)
|
||||
subscription.edit {
|
||||
appendAppGroups(
|
||||
targetApp = subscription.getApp(appId),
|
||||
groups = newGroups,
|
||||
)
|
||||
}
|
||||
val usedKeys = oldApp.groups.mapTo(mutableSetOf()) { it.key }
|
||||
val normalizedGroups = newGroups.map { group ->
|
||||
if (group.key in usedKeys) {
|
||||
val newKey = requireNotNull(usedKeys.maxOrNull()) + 1
|
||||
group.copy(key = newKey).also { usedKeys.add(newKey) }
|
||||
} else {
|
||||
group.also { usedKeys.add(it.key) }
|
||||
}
|
||||
}
|
||||
subscription.copy(
|
||||
apps = subscription.apps.toMutableList().apply {
|
||||
set(
|
||||
appIndex,
|
||||
oldApp.copy(groups = oldApp.groups + normalizedGroups),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
} else {
|
||||
// add global group
|
||||
val newGroup = try {
|
||||
RawSubscription.parseGlobalGroup(jsonObject)
|
||||
} catch (e: Exception) {
|
||||
LogUtils.d(e)
|
||||
error("非法规则\n${e.message}")
|
||||
}
|
||||
val newGroup = input.parseGlobalGroup()
|
||||
requiredSubscription.update { subscription ->
|
||||
checkGroupKeyName(subscription.globalGroups, newGroup)
|
||||
val normalizedGroup = if (
|
||||
subscription.globalGroups.any { it.key == newGroup.key }
|
||||
) {
|
||||
newGroup.copy(
|
||||
key = subscription.globalGroups.maxOf { it.key } + 1,
|
||||
)
|
||||
} else {
|
||||
newGroup
|
||||
}
|
||||
subscription.copy(
|
||||
globalGroups = subscription.globalGroups + normalizedGroup,
|
||||
)
|
||||
subscription.edit { appendGlobalGroup(newGroup) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,18 +156,10 @@ class UpsertRuleGroupVm(val route: UpsertRuleGroupRoute) : BaseViewModel() {
|
||||
} else {
|
||||
toast("添加成功")
|
||||
}
|
||||
return addedAppId
|
||||
}
|
||||
|
||||
init {
|
||||
addCloseable { clearJson5TransformationCache() }
|
||||
}
|
||||
}
|
||||
|
||||
private fun checkGroupKeyName(
|
||||
groups: List<RawSubscription.RawGroupProps>,
|
||||
newGroup: RawSubscription.RawGroupProps
|
||||
) {
|
||||
if (groups.any { it.name == newGroup.name }) {
|
||||
error("已存在同名「${newGroup.name}」规则")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ import li.songe.gkd.data.CategoryConfig
|
||||
import li.songe.gkd.data.ExcludeData
|
||||
import li.songe.gkd.data.RawSubscription
|
||||
import li.songe.gkd.data.SubsConfig
|
||||
import li.songe.gkd.data.edit
|
||||
import li.songe.gkd.db.DbSet
|
||||
import li.songe.gkd.ui.SubsGlobalGroupExcludeRoute
|
||||
import li.songe.gkd.ui.UpsertRuleGroupRoute
|
||||
@@ -319,27 +320,20 @@ class RuleGroupState(
|
||||
val groupKey = requireNotNull(state.groupKey)
|
||||
SubscriptionStore.update(state.subsId) { subscription ->
|
||||
if (state.appId == null) {
|
||||
if (subscription.globalGroups.none { it.key == groupKey }) {
|
||||
error("规则已不存在")
|
||||
subscription.edit {
|
||||
if (removeGlobalGroups { it.key == groupKey }.isEmpty()) {
|
||||
error("规则已不存在")
|
||||
}
|
||||
}
|
||||
subscription.copy(
|
||||
globalGroups = subscription.globalGroups.filter { it.key != groupKey },
|
||||
)
|
||||
} else {
|
||||
val appIndex = subscription.apps.indexOfFirst { it.id == state.appId }
|
||||
if (appIndex < 0) error("应用规则已不存在")
|
||||
val app = subscription.apps[appIndex]
|
||||
if (app.groups.none { it.key == groupKey }) {
|
||||
error("规则已不存在")
|
||||
subscription.edit {
|
||||
if (subscription.apps.none { it.id == state.appId }) {
|
||||
error("应用规则已不存在")
|
||||
}
|
||||
if (removeAppGroups(state.appId) { it.key == groupKey }.isEmpty()) {
|
||||
error("规则已不存在")
|
||||
}
|
||||
}
|
||||
subscription.copy(
|
||||
apps = subscription.apps.toMutableList().apply {
|
||||
set(
|
||||
appIndex,
|
||||
app.copy(groups = app.groups.filter { it.key != groupKey }),
|
||||
)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
241
app/src/test/kotlin/li/songe/gkd/data/SubscriptionEditorTest.kt
Normal file
241
app/src/test/kotlin/li/songe/gkd/data/SubscriptionEditorTest.kt
Normal file
@@ -0,0 +1,241 @@
|
||||
package li.songe.gkd.data
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertFalse
|
||||
import org.junit.Assert.assertSame
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class SubscriptionEditorTest {
|
||||
|
||||
@Test
|
||||
fun updatesArbitrarySubscriptionAndNestedFieldsInOneSnapshot() {
|
||||
val source = createSubscription(
|
||||
apps = listOf(RawSubscription.RawApp("app.id", "Old name")),
|
||||
categories = listOf(RawSubscription.RawCategory(0, "Old", null, null)),
|
||||
)
|
||||
|
||||
val edited = source.edit {
|
||||
update { copy(name = "Edited", author = "Author") }
|
||||
assertTrue(updateApp("app.id") { copy(name = "New name") })
|
||||
assertTrue(updateCategory(0) { copy(name = "New category", desc = "Desc") })
|
||||
}
|
||||
|
||||
assertEquals("Edited", edited.name)
|
||||
assertEquals("Author", edited.author)
|
||||
assertEquals("New name", edited.apps.single().name)
|
||||
assertEquals("New category", edited.categories.single().name)
|
||||
assertEquals("Desc", edited.categories.single().desc)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun missingStrictNodeIsNotCreated() {
|
||||
val source = createSubscription()
|
||||
|
||||
val edited = source.edit {
|
||||
assertFalse(updateApp("missing") { copy(name = "Unexpected") })
|
||||
assertFalse(updateCategory(1) { copy(name = "Unexpected") })
|
||||
}
|
||||
|
||||
assertSame(source, edited)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun removingLastAppGroupCanRemoveItsApp() {
|
||||
val source = RawSubscription.parse(
|
||||
"""
|
||||
{
|
||||
id: -2,
|
||||
name: 'Local',
|
||||
version: 0,
|
||||
apps: [{
|
||||
id: 'app.id',
|
||||
groups: [{ key: 0, name: 'Rule', rules: [] }],
|
||||
}],
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val edited = source.edit {
|
||||
val removed = removeAppGroups("app.id", removeAppIfEmpty = true) { it.key == 0 }
|
||||
assertEquals(1, removed.size)
|
||||
}
|
||||
|
||||
assertTrue(edited.apps.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun replacesAndRemovesAppAndGlobalGroupsByKey() {
|
||||
val source = RawSubscription.parse(
|
||||
"""
|
||||
{
|
||||
id: -2,
|
||||
name: 'Local',
|
||||
version: 0,
|
||||
globalGroups: [{ key: 1, name: 'Global', rules: [] }],
|
||||
apps: [{
|
||||
id: 'app.id',
|
||||
groups: [{ key: 2, name: 'App', rules: [] }],
|
||||
}],
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val edited = source.edit {
|
||||
val globalGroup = source.globalGroups.single()
|
||||
replaceGlobalGroup(
|
||||
groupKey = globalGroup.key,
|
||||
expectedGroup = globalGroup,
|
||||
newGroup = globalGroup.copy(name = "Edited global"),
|
||||
)
|
||||
val app = source.apps.single()
|
||||
val appGroup = app.groups.single()
|
||||
replaceAppGroup(
|
||||
targetApp = app,
|
||||
groupKey = appGroup.key,
|
||||
expectedGroup = appGroup,
|
||||
newGroup = appGroup.copy(name = "Edited app"),
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("Edited global", edited.globalGroups.single().name)
|
||||
assertEquals("Edited app", edited.apps.single().groups.single().name)
|
||||
|
||||
val removed = edited.edit {
|
||||
assertEquals(1, removeGlobalGroups { it.key == 1 }.size)
|
||||
assertEquals(1, removeAppGroups("app.id") { it.key == 2 }.size)
|
||||
}
|
||||
|
||||
assertTrue(removed.globalGroups.isEmpty())
|
||||
assertTrue(removed.apps.single().groups.isEmpty())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun subscriptionIdCannotBeChanged() {
|
||||
val source = createSubscription()
|
||||
|
||||
assertThrows(IllegalArgumentException::class.java) {
|
||||
source.edit { update { copy(id = 1) } }
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
fun appendingGroupsCreatesMissingAppAndNormalizesConflictingKeys() {
|
||||
val source = createSubscription(
|
||||
apps = listOf(
|
||||
RawSubscription.RawApp(
|
||||
id = "existing.app",
|
||||
name = null,
|
||||
groups = listOf(createAppGroup(0, "Existing")),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val edited = source.edit {
|
||||
appendAppGroups(
|
||||
source.apps.first { it.id == "existing.app" },
|
||||
listOf(createAppGroup(0, "First"), createAppGroup(1, "Second")),
|
||||
)
|
||||
appendAppGroups(
|
||||
RawSubscription.RawApp("new.app", "New app"),
|
||||
listOf(createAppGroup(0, "New")),
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals(
|
||||
listOf(0, 1, 2),
|
||||
edited.apps.first { it.id == "existing.app" }.groups.map { it.key },
|
||||
)
|
||||
val newApp = edited.apps.first { it.id == "new.app" }
|
||||
assertEquals("New app", newApp.name)
|
||||
assertEquals("New", newApp.groups.single().name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun duplicateGroupNamesAreRejectedWhenAppending() {
|
||||
val source = createSubscription(
|
||||
apps = listOf(
|
||||
RawSubscription.RawApp(
|
||||
id = "app.id",
|
||||
name = null,
|
||||
groups = listOf(createAppGroup(0, "Duplicate")),
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
val error = assertThrows(IllegalStateException::class.java) {
|
||||
source.edit {
|
||||
appendAppGroups(
|
||||
source.apps.single(),
|
||||
listOf(createAppGroup(1, "Duplicate")),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals("已存在同名「Duplicate」规则", error.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun replacingGroupCreatesMissingAppAndRejectsStaleEdits() {
|
||||
val originalGroup = createAppGroup(0, "Original")
|
||||
val replacement = createAppGroup(0, "Replacement")
|
||||
val missingAppEdited = createSubscription().edit {
|
||||
replaceAppGroup(
|
||||
targetApp = RawSubscription.RawApp("missing.app", "Missing app"),
|
||||
groupKey = 0,
|
||||
expectedGroup = originalGroup,
|
||||
newGroup = replacement,
|
||||
)
|
||||
}
|
||||
|
||||
assertEquals("missing.app", missingAppEdited.apps.single().id)
|
||||
assertEquals("Missing app", missingAppEdited.apps.single().name)
|
||||
assertEquals("Replacement", missingAppEdited.apps.single().groups.single().name)
|
||||
|
||||
val changedGroup = createAppGroup(0, "Changed elsewhere")
|
||||
val changedSource = createSubscription(
|
||||
apps = listOf(RawSubscription.RawApp("app.id", null, listOf(changedGroup))),
|
||||
)
|
||||
val error = assertThrows(IllegalStateException::class.java) {
|
||||
changedSource.edit {
|
||||
replaceAppGroup(
|
||||
targetApp = changedSource.apps.single(),
|
||||
groupKey = 0,
|
||||
expectedGroup = originalGroup,
|
||||
newGroup = replacement,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
assertEquals("规则已发生变化,请重新编辑", error.message)
|
||||
}
|
||||
|
||||
private fun createSubscription(
|
||||
apps: List<RawSubscription.RawApp> = emptyList(),
|
||||
categories: List<RawSubscription.RawCategory> = emptyList(),
|
||||
) = RawSubscription(
|
||||
id = -2,
|
||||
name = "Local",
|
||||
version = 0,
|
||||
apps = apps,
|
||||
categories = categories,
|
||||
)
|
||||
|
||||
private fun createAppGroup(
|
||||
key: Int,
|
||||
name: String,
|
||||
): RawSubscription.RawAppGroup = RawSubscription.parse(
|
||||
"""
|
||||
{
|
||||
id: -2,
|
||||
name: 'Local',
|
||||
version: 0,
|
||||
apps: [{
|
||||
id: 'app.id',
|
||||
groups: [{ key: $key, name: '$name', rules: [] }],
|
||||
}],
|
||||
}
|
||||
""".trimIndent(),
|
||||
).apps.single().groups.single()
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
package li.songe.gkd.data
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Assert.assertThrows
|
||||
import org.junit.Assert.assertTrue
|
||||
import org.junit.Test
|
||||
|
||||
class SubscriptionInputParserTest {
|
||||
|
||||
@Test
|
||||
fun singleGroupGetsDefaultKey() {
|
||||
val input = SubscriptionInputParser.parse(
|
||||
"""
|
||||
{
|
||||
name: 'Rule',
|
||||
rules: [],
|
||||
}
|
||||
""".trimIndent(),
|
||||
defaultGroupKey = 7,
|
||||
)
|
||||
|
||||
val appGroup = input.parseAppGroup("app.id")
|
||||
val globalGroup = input.parseGlobalGroup()
|
||||
|
||||
assertEquals(7, appGroup.key)
|
||||
assertEquals(7, globalGroup.key)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fullAppGetsUniqueKeysWithoutDroppingGroups() {
|
||||
val input = SubscriptionInputParser.parse(
|
||||
"""
|
||||
{
|
||||
id: 'app.id',
|
||||
groups: [
|
||||
{ name: 'First', rules: [] },
|
||||
{ key: 2, name: 'Explicit', rules: [] },
|
||||
{ name: 'Second', rules: [] },
|
||||
],
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val groups = input.parseAppGroups("app.id")
|
||||
|
||||
assertEquals(listOf("First", "Explicit", "Second"), groups.map { it.name })
|
||||
assertEquals(listOf(0, 2, 1), groups.map { it.key })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun quotedNumericKeyIsReservedWhenFillingMissingKeys() {
|
||||
val input = SubscriptionInputParser.parse(
|
||||
"""
|
||||
{
|
||||
id: 'app.id',
|
||||
groups: [
|
||||
{ key: '0', name: 'Quoted', rules: [] },
|
||||
{ name: 'Filled', rules: [] },
|
||||
],
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val groups = input.parseAppGroups("app.id")
|
||||
|
||||
assertEquals(listOf("Quoted", "Filled"), groups.map { it.name })
|
||||
assertEquals(listOf(0, 1), groups.map { it.key })
|
||||
}
|
||||
|
||||
@Test
|
||||
fun appGroupInputAcceptsFullAppAndSingleGroup() {
|
||||
val fullApp = SubscriptionInputParser.parse(
|
||||
"""
|
||||
{
|
||||
id: 'app.id',
|
||||
groups: [{ key: 3, name: 'Wrapped', rules: [] }],
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
val singleGroup = SubscriptionInputParser.parse(
|
||||
"{ key: 4, name: 'Single', rules: [] }",
|
||||
)
|
||||
|
||||
assertEquals("Wrapped", fullApp.parseAppGroup("app.id").name)
|
||||
assertEquals("Single", singleGroup.parseAppGroup("app.id").name)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun fullAppMustMatchExpectedAppId() {
|
||||
val input = SubscriptionInputParser.parse(
|
||||
"""
|
||||
{
|
||||
id: 'other.app',
|
||||
groups: [{ key: 0, name: 'Rule', rules: [] }],
|
||||
}
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
val error = assertThrows(IllegalStateException::class.java) {
|
||||
input.parseAppGroups("app.id")
|
||||
}
|
||||
|
||||
assertEquals("非法规则\nid与当前应用不一致", error.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun appInputRequiresAtLeastOneGroup() {
|
||||
val input = SubscriptionInputParser.parse("{ id: 'app.id', groups: [] }")
|
||||
|
||||
val error = assertThrows(IllegalStateException::class.java) {
|
||||
input.parseApp()
|
||||
}
|
||||
|
||||
assertEquals("非法规则\n至少输入一个规则", error.message)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun reportsSyntaxAndTopLevelTypeErrors() {
|
||||
val syntaxError = assertThrows(IllegalStateException::class.java) {
|
||||
SubscriptionInputParser.parse("{")
|
||||
}
|
||||
val typeError = assertThrows(IllegalStateException::class.java) {
|
||||
SubscriptionInputParser.parse("[]")
|
||||
}
|
||||
|
||||
assertTrue(syntaxError.message.orEmpty().startsWith("非法格式\n"))
|
||||
assertEquals("规则应为对象格式", typeError.message)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user