feat: namespaceの扱いの改善
This commit is contained in:
parent
29dc1f10dd
commit
a4f6e8e236
19
README.md
19
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
|
||||
|
|
|
|||
|
|
@ -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 =
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user