diff --git a/README.md b/README.md index 7e5429c..2e6aaba 100644 --- a/README.md +++ b/README.md @@ -24,8 +24,8 @@ class ExamplePlugin : JavaPlugin() { // Link to a helper node defined elsewhere under the command branch: child("helper") - // Link to a permission outside the current branch by using the absolute helper: - childAbsolute("tools.repair") + // Link to a permission outside the current branch (must be fully-qualified): + childAbsolute("example.tools.repair") node("cooldown") { description = "Allows /example cooldown tweaks" @@ -38,7 +38,7 @@ class ExamplePlugin : JavaPlugin() { } node("tools.repair") { - description = "Allows /example tools repair (linked with childAbsolute)" + description = "Allows /example tools repair (linked with childAbsolute(\"example.tools.repair\"))" } } @@ -93,12 +93,12 @@ mutable.node("command") { } child("helper", value = false) // unlink helper if present } -mutable.remove("command.legacy") +mutable.removeNode("command.legacy") permits.applyTree(mutable.build()) ``` -The mutable API mirrors the DSL (`node`, `child`, `childAbsolute`, `remove`, `removeChild`, etc.) so you can +The mutable API mirrors the DSL (`node`, `child`, `childAbsolute`, `removeNode`, `renameNode`, etc.) so you can stage edits procedurally before ever touching `MutationSession`. ### Concepts @@ -106,16 +106,17 @@ stage edits procedurally before ever touching `MutationSession`. - **Permission tree** – immutable graph of `PermissionNode`s. Nodes specify description, default value, boolean children map, optional tags, and the `wildcard` flag (enabled by default) that makes the library create/update `namespace.path.*` aggregate permissions automatically. -- **DSL** – `permissionTree("namespace") { ... }` ensures consistent prefixes and validation (no cycles). +- **DSL** – `permissionTree("namespace") { ... }` ensures consistent prefixes and validation (no cycles). Top-level `node("command")` / `remove("command")` treat `command` as relative to that namespace, so you never include the namespace manually at the root. - **Nested nodes** – `node("command") { node("reload") { ... } }` automatically produces `namespace.command` and `namespace.command.reload` plus wires the parent/child relationship so you don't have to repeat the full id. - **Flexible references** – `child("reload")`, `node("command") { node("reload") { ... } }`, or even `node("command.reload")` inside `edit` all resolve to the same node; children are auto-created on first reference but you can demand explicit nodes by adding a `node` block later, and you can unlink - specific children via `node("command") { removeChild("cooldown") }` without deleting the underlying node. - Nested `child(...)` calls are relative to the current node by default, while `childAbsolute(...)` lets you - point at any fully-qualified permission ID within the namespace. + specific children via `node("command") { removeNode("cooldown") }` and the entire subtree disappears. + Nested `child(...)` calls are relative to the current node by default, while `childAbsolute(...)` now + expects a fully-qualified permission ID (e.g., `example.tools.repair`) so you can also point at nodes in + other namespaces. - **PermissionRegistry** – calculates a diff between snapshots and performs the minimum additions, removals, or updates via Bukkit's `PluginManager`. - **Wildcards** – with `wildcard = true`, the generated `namespace.command.*` child always exists and stays diff --git a/src/main/kotlin/net/hareworks/permits_lib/domain/MutablePermissionTree.kt b/src/main/kotlin/net/hareworks/permits_lib/domain/MutablePermissionTree.kt index ca0f95b..1693446 100644 --- a/src/main/kotlin/net/hareworks/permits_lib/domain/MutablePermissionTree.kt +++ b/src/main/kotlin/net/hareworks/permits_lib/domain/MutablePermissionTree.kt @@ -11,37 +11,36 @@ class MutablePermissionTree internal constructor( private val drafts: MutableMap ) { 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() + 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>() + 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 } } } diff --git a/src/main/kotlin/net/hareworks/permits_lib/dsl/PermissionNodeBuilder.kt b/src/main/kotlin/net/hareworks/permits_lib/dsl/PermissionNodeBuilder.kt index 52a4455..153965a 100644 --- a/src/main/kotlin/net/hareworks/permits_lib/dsl/PermissionNodeBuilder.kt +++ b/src/main/kotlin/net/hareworks/permits_lib/dsl/PermissionNodeBuilder.kt @@ -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) } diff --git a/src/main/kotlin/net/hareworks/permits_lib/dsl/PermissionTreeBuilder.kt b/src/main/kotlin/net/hareworks/permits_lib/dsl/PermissionTreeBuilder.kt index 2ae73e5..b697e97 100644 --- a/src/main/kotlin/net/hareworks/permits_lib/dsl/PermissionTreeBuilder.kt +++ b/src/main/kotlin/net/hareworks/permits_lib/dsl/PermissionTreeBuilder.kt @@ -11,21 +11,25 @@ class PermissionTreeBuilder internal constructor( private val drafts = linkedMapOf() 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 =