diff --git a/README.md b/README.md index 2e6aaba..765192e 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,20 @@ instances and keep `PermissionAttachment`s in sync. ## Usage ```kotlin +import net.hareworks.permits_lib.domain.NodeRegistration + class ExamplePlugin : JavaPlugin() { private val permits = PermitsLib.session(this) override fun onEnable() { val tree = permissionTree("example") { - node("command") { + node("command", NodeRegistration.STRUCTURAL) { description = "Access to all example commands" defaultValue = PermissionDefault.OP - node("reload") { + node("reload", NodeRegistration.PERMISSION) { description = "Allows /example reload (permission example.command.reload)" + wildcard = true // include reload in example.command.* } // Link to a helper node defined elsewhere under the command branch: @@ -27,17 +30,17 @@ class ExamplePlugin : JavaPlugin() { // Link to a permission outside the current branch (must be fully-qualified): childAbsolute("example.tools.repair") - node("cooldown") { + node("cooldown", NodeRegistration.PERMISSION) { description = "Allows /example cooldown tweaks" - wildcard = false // opt-out if you do not want example.command.* to include it + wildcard = true // opt in so example.command.* grants cooldown } } - node("command.helper") { + node("command.helper", NodeRegistration.PERMISSION) { description = "Allows /example helper (referenced via child(\"helper\"))" } - node("tools.repair") { + node("tools.repair", NodeRegistration.PERMISSION) { description = "Allows /example tools repair (linked with childAbsolute(\"example.tools.repair\"))" } } @@ -47,7 +50,7 @@ class ExamplePlugin : JavaPlugin() { // The tree above materializes as permissions such as: // example.command, example.command.reload, example.command.helper, example.command.cooldown, // example.tools.repair, - // plus the auto-generated example.command.* wildcard that references every child (visible if you + // plus the auto-generated example.command.* wildcard (because reload/cooldown set wildcard = true) // export to plugin.yml or inspect Bukkit's /permissions output). configureRuntimePermissions() @@ -61,15 +64,15 @@ class ExamplePlugin : JavaPlugin() { // Later in runtime you can mutate the previously applied structure without rebuilding it: permits.edit("example") { // Update an existing node and link it to new children - node("command") { + node("command", NodeRegistration.STRUCTURAL) { description = "Admins for every command path" - node("debug") { + node("debug", NodeRegistration.PERMISSION) { description = "Allows /example debug" defaultValue = PermissionDefault.OP } } // Remove deprecated permissions entirely - remove("command.cooldown") + removeNode("command.cooldown") } } } @@ -82,12 +85,14 @@ mutate it procedurally, and then apply the result: ```kotlin val baseTree = permissionTree("example") { - node("command") { node("reload") } + node("command", NodeRegistration.STRUCTURAL) { + node("reload", NodeRegistration.PERMISSION) + } } val mutable = MutablePermissionTree.from(baseTree) -mutable.node("command") { - node("debug") { +mutable.node("command", NodeRegistration.STRUCTURAL) { + node("debug", NodeRegistration.PERMISSION) { description = "Allows /example debug" defaultValue = PermissionDefault.OP } @@ -104,24 +109,25 @@ stage edits procedurally before ever touching `MutationSession`. ### Concepts - **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). 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 + boolean children map, optional tags, and the `wildcard` flag (disabled by default) that, when enabled, + makes the library create/update `namespace.path.*` aggregate permissions automatically. +- **DSL** – `permissionTree("namespace") { ... }` ensures consistent prefixes and validation (no cycles). Every `node("command", NodeRegistration.PERMISSION)` (or `.STRUCTURAL`) is relative to that namespace, so you never include the namespace manually at the root. +- **Nested nodes** – `node("command", NodeRegistration.STRUCTURAL) { node("reload", NodeRegistration.PERMISSION) { ... } }` 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 +- **Flexible references** – `child("reload")`, `node("command", NodeRegistration.STRUCTURAL) { node("reload", NodeRegistration.PERMISSION) { ... } }`, or + even `node("command.reload", NodeRegistration.PERMISSION)` 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") { removeNode("cooldown") }` and the entire subtree disappears. + specific children via `node("command", NodeRegistration.STRUCTURAL) { removeNode("cooldown") }` and the entire subtree disappears. +- **Node registration** – `NodeRegistration.PERMISSION` materializes the node as a Bukkit permission, while `NodeRegistration.STRUCTURAL` keeps it purely for grouping (still participates in wildcard aggregation) so you can avoid ambiguous intermediate permissions like `hoge.command`. 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 - in sync so granting `example.command.*` automatically grants every nested node; set it to `false` to opt - out for specific permissions. +- **Wildcards** – opt-in via `wildcard = true` to have the generated `namespace.command.*` child kept in sync, + so granting `example.command.*` automatically grants every nested node you marked; leave it `false` (default) + to keep nodes out of the wildcard. - **Mutable edits** – `permits.edit { ... }` clones the currently registered tree, lets you mutate nodes imperatively, re-validates, and only pushes the structural diff to Bukkit. - **AttachmentSynchronizer** – manages identity-based `PermissionAttachment`s and exposes high-level 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 1693446..5b3d1ca 100644 --- a/src/main/kotlin/net/hareworks/permits_lib/domain/MutablePermissionTree.kt +++ b/src/main/kotlin/net/hareworks/permits_lib/domain/MutablePermissionTree.kt @@ -10,10 +10,11 @@ class MutablePermissionTree internal constructor( private val namespace: String, private val drafts: MutableMap ) { - fun node(id: String, block: MutableNode.() -> Unit = {}): MutableNode { + fun node(id: String, registration: NodeRegistration, block: MutableNode.() -> Unit = {}): MutableNode { require(id.isNotBlank()) { "Node id must not be blank." } val permissionId = PermissionId.of("$namespace.${id.lowercase()}") val draft = drafts.getOrPut(permissionId) { PermissionNodeDraft(permissionId) } + draft.registration = registration return MutableNode(permissionId, draft).apply(block) } @@ -63,6 +64,12 @@ class MutablePermissionTree internal constructor( draft.wildcard = value } + var registration: NodeRegistration + get() = draft.registration + set(value) { + draft.registration = value + } + val tags: MutableSet get() = draft.tags @@ -83,11 +90,12 @@ class MutablePermissionTree internal constructor( draft.children[permissionId] = value } - fun node(id: String, block: MutableNode.() -> Unit = {}) { + fun node(id: String, registration: NodeRegistration, block: MutableNode.() -> Unit = {}) { 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) } + childDraft.registration = registration MutableNode(permissionId, childDraft).apply(block) } @@ -151,7 +159,8 @@ class MutablePermissionTree internal constructor( defaultValue = draft.defaultValue, children = draft.children.toMutableMap(), tags = draft.tags.toMutableSet(), - wildcard = draft.wildcard + wildcard = draft.wildcard, + registration = draft.registration ) drafts[newId] = newDraft } diff --git a/src/main/kotlin/net/hareworks/permits_lib/domain/NodeRegistration.kt b/src/main/kotlin/net/hareworks/permits_lib/domain/NodeRegistration.kt new file mode 100644 index 0000000..20c6f8d --- /dev/null +++ b/src/main/kotlin/net/hareworks/permits_lib/domain/NodeRegistration.kt @@ -0,0 +1,10 @@ +package net.hareworks.permits_lib.domain + +/** + * Declares whether a DSL node should materialize as an actual Bukkit permission or behave as a + * purely structural placeholder (still participates in relationships/wildcards). + */ +enum class NodeRegistration(val registersPermission: Boolean) { + PERMISSION(true), + STRUCTURAL(false) +} diff --git a/src/main/kotlin/net/hareworks/permits_lib/domain/PermissionNode.kt b/src/main/kotlin/net/hareworks/permits_lib/domain/PermissionNode.kt index 1dbcaf7..930eb71 100644 --- a/src/main/kotlin/net/hareworks/permits_lib/domain/PermissionNode.kt +++ b/src/main/kotlin/net/hareworks/permits_lib/domain/PermissionNode.kt @@ -14,7 +14,8 @@ data class PermissionNode( val defaultValue: PermissionDefault = PermissionDefault.FALSE, val children: Map = emptyMap(), val tags: Set = emptySet(), - val wildcard: Boolean = true + val wildcard: Boolean = false, + val registration: NodeRegistration = NodeRegistration.PERMISSION ) { init { require(children.keys.none { it == id }) { "Permission node cannot be a child of itself." } diff --git a/src/main/kotlin/net/hareworks/permits_lib/domain/PermissionNodeDraft.kt b/src/main/kotlin/net/hareworks/permits_lib/domain/PermissionNodeDraft.kt index 7b84625..9df8a8c 100644 --- a/src/main/kotlin/net/hareworks/permits_lib/domain/PermissionNodeDraft.kt +++ b/src/main/kotlin/net/hareworks/permits_lib/domain/PermissionNodeDraft.kt @@ -8,7 +8,8 @@ internal data class PermissionNodeDraft( var defaultValue: PermissionDefault = PermissionDefault.FALSE, val children: MutableMap = linkedMapOf(), val tags: MutableSet = linkedSetOf(), - var wildcard: Boolean = true + var wildcard: Boolean = false, + var registration: NodeRegistration = NodeRegistration.PERMISSION ) { fun toNode(): PermissionNode = PermissionNode( @@ -17,7 +18,8 @@ internal data class PermissionNodeDraft( defaultValue = defaultValue, children = children.toMap(), tags = tags.toSet(), - wildcard = wildcard + wildcard = wildcard, + registration = registration ) companion object { @@ -28,7 +30,8 @@ internal data class PermissionNodeDraft( defaultValue = node.defaultValue, children = node.children.toMutableMap(), tags = node.tags.toMutableSet(), - wildcard = node.wildcard + wildcard = node.wildcard, + registration = node.registration ) } } diff --git a/src/main/kotlin/net/hareworks/permits_lib/domain/PermissionTree.kt b/src/main/kotlin/net/hareworks/permits_lib/domain/PermissionTree.kt index d088627..2759876 100644 --- a/src/main/kotlin/net/hareworks/permits_lib/domain/PermissionTree.kt +++ b/src/main/kotlin/net/hareworks/permits_lib/domain/PermissionTree.kt @@ -15,7 +15,8 @@ class PermissionTree internal constructor( operator fun get(id: PermissionId): PermissionNode? = nodes[id] - fun toSnapshot(): TreeSnapshot = TreeSnapshot(nodes) + fun toSnapshot(): TreeSnapshot = + TreeSnapshot(nodes.filterValues { it.registration.registersPermission }) companion object { fun empty(namespace: String): PermissionTree = PermissionTree(namespace, emptyMap()) diff --git a/src/main/kotlin/net/hareworks/permits_lib/domain/TreeSnapshot.kt b/src/main/kotlin/net/hareworks/permits_lib/domain/TreeSnapshot.kt index a121f46..045a02c 100644 --- a/src/main/kotlin/net/hareworks/permits_lib/domain/TreeSnapshot.kt +++ b/src/main/kotlin/net/hareworks/permits_lib/domain/TreeSnapshot.kt @@ -29,6 +29,7 @@ class TreeSnapshot internal constructor( digest.update(tag.toByteArray()) } digest.update(if (node.wildcard) 1 else 0) + digest.update(node.registration.name.toByteArray()) } return digest.digest().joinToString("") { "%02x".format(it) } } 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 153965a..5aec90c 100644 --- a/src/main/kotlin/net/hareworks/permits_lib/dsl/PermissionNodeBuilder.kt +++ b/src/main/kotlin/net/hareworks/permits_lib/dsl/PermissionNodeBuilder.kt @@ -1,5 +1,6 @@ package net.hareworks.permits_lib.dsl +import net.hareworks.permits_lib.domain.NodeRegistration import net.hareworks.permits_lib.domain.PermissionId import net.hareworks.permits_lib.domain.PermissionNodeDraft import org.bukkit.permissions.PermissionDefault @@ -27,6 +28,12 @@ class PermissionNodeBuilder internal constructor( draft.wildcard = value } + var registration: NodeRegistration + get() = draft.registration + set(value) { + draft.registration = value + } + fun tag(value: String) { if (value.isNotBlank()) { draft.tags += value.trim() @@ -52,12 +59,16 @@ class PermissionNodeBuilder internal constructor( * Declares a nested node whose id is derived from the current node: * * ``` - * node("command") { - * node("reload") { ... } // -> namespace.command.reload + * node("command", NodeRegistration.STRUCTURAL) { + * node("reload", NodeRegistration.PERMISSION) { ... } // -> namespace.command.reload * } * ``` */ - fun node(id: String, block: PermissionNodeBuilder.() -> Unit = {}) { - treeBuilder.nestedNode(draft, id, block) + fun node( + id: String, + registration: NodeRegistration, + block: PermissionNodeBuilder.() -> Unit = {} + ) { + treeBuilder.nestedNode(draft, id, registration, block) } } 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 b697e97..1698c8c 100644 --- a/src/main/kotlin/net/hareworks/permits_lib/dsl/PermissionTreeBuilder.kt +++ b/src/main/kotlin/net/hareworks/permits_lib/dsl/PermissionTreeBuilder.kt @@ -1,5 +1,6 @@ package net.hareworks.permits_lib.dsl +import net.hareworks.permits_lib.domain.NodeRegistration import net.hareworks.permits_lib.domain.PermissionId import net.hareworks.permits_lib.domain.PermissionNodeDraft import net.hareworks.permits_lib.domain.PermissionTree @@ -10,10 +11,15 @@ class PermissionTreeBuilder internal constructor( ) { private val drafts = linkedMapOf() - fun node(id: String, block: PermissionNodeBuilder.() -> Unit = {}) { + fun node( + id: String, + registration: NodeRegistration, + block: PermissionNodeBuilder.() -> Unit = {} + ) { require(id.isNotBlank()) { "Node id must not be blank." } val permissionId = PermissionId.of("$namespace.${id.lowercase()}") val draft = drafts.getOrPut(permissionId) { PermissionNodeDraft(permissionId) } + draft.registration = registration PermissionNodeBuilder(this, draft).apply(block) } @@ -27,7 +33,7 @@ class PermissionTreeBuilder internal constructor( require(id.isNotBlank()) { "Child id must not be blank." } "${parent.id.value}.${id.lowercase()}" } else { - id.lowercase() + normalizeAbsolute(id) } val permissionId = PermissionId.of(target) parent.children[permissionId] = value @@ -48,17 +54,24 @@ class PermissionTreeBuilder internal constructor( internal fun nestedNode( parent: PermissionNodeDraft, id: String, + registration: NodeRegistration, block: PermissionNodeBuilder.() -> Unit ) { 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) } + draft.registration = registration PermissionNodeBuilder(this, draft).apply(block) } fun build(): PermissionTree = PermissionTree.from(namespace, drafts.mapValues { it.value.toNode() }) + + private fun normalizeAbsolute(id: String): String { + require(id.isNotBlank()) { "Absolute permission id must not be blank." } + return id.lowercase() + } } fun permissionTree(namespace: String, block: PermissionTreeBuilder.() -> Unit): PermissionTree =