feat: namespaceの扱いの改善

This commit is contained in:
2025-12-04 04:41:37 +09:00
parent 29dc1f10dd
commit a4f6e8e236
4 changed files with 126 additions and 51 deletions
@@ -11,37 +11,36 @@ class MutablePermissionTree internal constructor(
private val drafts: MutableMap<PermissionId, PermissionNodeDraft>
) {
fun node(id: String, block: MutableNode.() -> Unit = {}): MutableNode {
val permissionId = PermissionId.of(qualify(id))
require(id.isNotBlank()) { "Node id must not be blank." }
val permissionId = PermissionId.of("$namespace.${id.lowercase()}")
val draft = drafts.getOrPut(permissionId) { PermissionNodeDraft(permissionId) }
return MutableNode(permissionId, draft).apply(block)
}
fun remove(id: String) {
val permissionId = PermissionId.of(qualify(id))
drafts.remove(permissionId)
drafts.values.forEach { it.children.remove(permissionId) }
fun removeNode(id: String) {
require(id.isNotBlank()) { "Node id must not be blank." }
val permissionId = PermissionId.of("$namespace.${id.lowercase()}")
removeSubtree(permissionId)
}
fun contains(id: String): Boolean = drafts.containsKey(PermissionId.of(qualify(id)))
fun renameNode(oldId: String, newId: String) {
require(oldId.isNotBlank()) { "Old node id must not be blank." }
require(newId.isNotBlank()) { "New node id must not be blank." }
val oldPermissionId = PermissionId.of("$namespace.${oldId.lowercase()}")
val newPermissionId = PermissionId.of("$namespace.${newId.lowercase()}")
renameSubtree(oldPermissionId, newPermissionId)
}
fun contains(id: String): Boolean {
require(id.isNotBlank()) { "Node id must not be blank." }
return drafts.containsKey(PermissionId.of("$namespace.${id.lowercase()}"))
}
fun build(): PermissionTree {
val nodes = drafts.mapValues { it.value.toNode() }
return PermissionTree.from(namespace, nodes)
}
private fun qualify(id: String): String =
if (id.startsWith(namespace)) id else "$namespace.$id"
private fun qualifyRelative(parent: PermissionId, childSegment: String): String {
val normalized = childSegment.trim().lowercase().trimStart('.')
require(normalized.isNotEmpty()) { "Child id must not be blank." }
return if (normalized.startsWith(namespace)) {
normalized
} else {
"${parent.value}.$normalized"
}
}
inner class MutableNode internal constructor(
val id: PermissionId,
private val draft: PermissionNodeDraft
@@ -74,24 +73,101 @@ class MutablePermissionTree internal constructor(
}
fun child(id: String, value: Boolean = true) {
val permissionId = PermissionId.of(qualifyRelative(this.id, id))
require(id.isNotBlank()) { "Child id must not be blank." }
val permissionId = PermissionId.of("${this.id.value}.${id.lowercase()}")
draft.children[permissionId] = value
}
fun childAbsolute(id: String, value: Boolean = true) {
val permissionId = PermissionId.of(qualify(id))
val permissionId = PermissionId.of(id.lowercase())
draft.children[permissionId] = value
}
fun node(id: String, block: MutableNode.() -> Unit = {}) {
val permissionId = PermissionId.of(qualifyRelative(this.id, id))
require(id.isNotBlank()) { "Node id must not be blank." }
val permissionId = PermissionId.of("${this.id.value}.${id.lowercase()}")
draft.children[permissionId] = true
val childDraft = drafts.getOrPut(permissionId) { PermissionNodeDraft(permissionId) }
MutableNode(permissionId, childDraft).apply(block)
}
fun removeChild(id: String) {
draft.children.remove(PermissionId.of(qualify(id)))
fun removeNode(id: String) {
require(id.isNotBlank()) { "Node id must not be blank." }
val permissionId = PermissionId.of("${this.id.value}.${id.lowercase()}")
removeSubtree(permissionId)
}
fun renameNode(oldId: String, newId: String) {
require(oldId.isNotBlank()) { "Old node id must not be blank." }
require(newId.isNotBlank()) { "New node id must not be blank." }
val oldPermissionId = PermissionId.of("${this.id.value}.${oldId.lowercase()}")
val newPermissionId = PermissionId.of("${this.id.value}.${newId.lowercase()}")
renameSubtree(oldPermissionId, newPermissionId)
}
}
private fun removeSubtree(rootId: PermissionId) {
val prefix = "${rootId.value}."
val targets = drafts.keys.filter { key ->
key.value == rootId.value || key.value.startsWith(prefix)
}.toSet()
if (targets.isEmpty()) return
targets.forEach { drafts.remove(it) }
drafts.values.forEach { draft ->
val iterator = draft.children.entries.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
if (entry.key in targets) {
iterator.remove()
}
}
}
}
private fun renameSubtree(oldRoot: PermissionId, newRoot: PermissionId) {
if (oldRoot == newRoot) return
val prefix = "${oldRoot.value}."
val affected = drafts.keys.filter { key ->
key.value == oldRoot.value || key.value.startsWith(prefix)
}
if (affected.isEmpty()) return
val affectedSet = affected.toSet()
val mapping = linkedMapOf<PermissionId, PermissionId>()
affected.forEach { oldId ->
val suffix = oldId.value.removePrefix(oldRoot.value)
val newValue = newRoot.value + suffix
val newId = PermissionId.of(newValue)
if (!affectedSet.contains(newId) && drafts.containsKey(newId)) {
error("Cannot rename '${oldRoot.value}' to '${newRoot.value}' because '$newValue' already exists.")
}
mapping[oldId] = newId
}
mapping.forEach { (oldId, newId) ->
val draft = drafts.remove(oldId) ?: return@forEach
val newDraft = PermissionNodeDraft(
id = newId,
description = draft.description,
defaultValue = draft.defaultValue,
children = draft.children.toMutableMap(),
tags = draft.tags.toMutableSet(),
wildcard = draft.wildcard
)
drafts[newId] = newDraft
}
drafts.values.forEach { draft ->
val pending = mutableListOf<Pair<PermissionId, Boolean>>()
val iterator = draft.children.entries.iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
val replacement = mapping[entry.key]
if (replacement != null) {
iterator.remove()
pending += replacement to entry.value
}
}
pending.forEach { (id, value) -> draft.children[id] = value }
}
}
@@ -41,6 +41,9 @@ class PermissionNodeBuilder internal constructor(
treeBuilder.childAbsolute(draft, id.value, value)
}
/**
* Links to a fully-qualified permission id. The provided [id] must already include its namespace.
*/
fun childAbsolute(id: String, value: Boolean = true) {
treeBuilder.childAbsolute(draft, id, value)
}
@@ -11,21 +11,25 @@ class PermissionTreeBuilder internal constructor(
private val drafts = linkedMapOf<PermissionId, PermissionNodeDraft>()
fun node(id: String, block: PermissionNodeBuilder.() -> Unit = {}) {
val permissionId = PermissionId.of(qualify(id))
require(id.isNotBlank()) { "Node id must not be blank." }
val permissionId = PermissionId.of("$namespace.${id.lowercase()}")
val draft = drafts.getOrPut(permissionId) { PermissionNodeDraft(permissionId) }
PermissionNodeBuilder(this, draft).apply(block)
}
internal fun qualify(id: String): String =
if (id.startsWith(namespace)) id else "$namespace.$id"
internal fun child(
parent: PermissionNodeDraft,
id: String,
value: Boolean,
relative: Boolean
) {
val permissionId = PermissionId.of(if (relative) qualifyRelative(parent.id, id) else qualify(id))
val target = if (relative) {
require(id.isNotBlank()) { "Child id must not be blank." }
"${parent.id.value}.${id.lowercase()}"
} else {
id.lowercase()
}
val permissionId = PermissionId.of(target)
parent.children[permissionId] = value
}
@@ -46,24 +50,15 @@ class PermissionTreeBuilder internal constructor(
id: String,
block: PermissionNodeBuilder.() -> Unit
) {
val permissionId = PermissionId.of(qualifyRelative(parent.id, id))
parent.children[permissionId] = true
val draft = drafts.getOrPut(permissionId) { PermissionNodeDraft(permissionId) }
require(id.isNotBlank()) { "Nested node id must not be blank." }
val composedId = PermissionId.of("${parent.id.value}.${id.lowercase()}")
parent.children[composedId] = true
val draft = drafts.getOrPut(composedId) { PermissionNodeDraft(composedId) }
PermissionNodeBuilder(this, draft).apply(block)
}
fun build(): PermissionTree =
PermissionTree.from(namespace, drafts.mapValues { it.value.toNode() })
private fun qualifyRelative(parent: PermissionId, childSegment: String): String {
val normalized = childSegment.trim().lowercase().trimStart('.')
require(normalized.isNotEmpty()) { "Child id must not be blank." }
return if (normalized.startsWith(namespace)) {
normalized
} else {
"${parent.value}.$normalized"
}
}
}
fun permissionTree(namespace: String, block: PermissionTreeBuilder.() -> Unit): PermissionTree =