diff --git a/README.md b/README.md index b8b0ece..cf12d0e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,9 @@ class ExamplePlugin : JavaPlugin() { node("command", NodeRegistration.STRUCTURAL) { description = "Access to all example commands" defaultValue = PermissionDefault.OP - wildcard = true // create example.command.* + wildcard { + exclude("cooldown") // example.command.* will skip cooldown + } node("reload", NodeRegistration.PERMISSION) { description = "Allows /example reload (permission example.command.reload)" @@ -32,7 +34,6 @@ class ExamplePlugin : JavaPlugin() { node("cooldown", NodeRegistration.PERMISSION) { description = "Allows /example cooldown tweaks" - wildcard = false // keep cooldown out of example.command.* } } @@ -50,7 +51,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 (command opted in while cooldown did not). + // plus the auto-generated example.command.* wildcard (command opted in, cooldown was excluded). // export to plugin.yml or inspect Bukkit's /permissions output). configureRuntimePermissions() @@ -96,12 +97,12 @@ val baseTree = permissionTree("example") { val mutable = MutablePermissionTree.from(baseTree) mutable.node("command", NodeRegistration.STRUCTURAL) { wildcard = true + excludeWildcardChild("helper") // keep helper out of command.* node("debug", NodeRegistration.PERMISSION) { description = "Allows /example debug" defaultValue = PermissionDefault.OP wildcard = true } - child("helper", value = false) // unlink helper if present } mutable.removeNode("command.legacy") @@ -130,9 +131,17 @@ stage edits procedurally before ever touching `MutationSession`. other namespaces. - **PermissionRegistry** – calculates a diff between snapshots and performs the minimum additions, removals, or updates via Bukkit's `PluginManager`. -- **Wildcards** – disabled by default; opt in by setting `wildcard = true` on any permission you want pulled - into its parent `namespace.command.*`. Enabled nodes automatically add their wildcard descendants (e.g., - `example.command.debug.*`) so granting a parent wildcard cascades through the tree. +- **Wildcards** – disabled by default; opt in via `wildcard = true` or the richer `wildcard { ... }` block. + The block automatically enables the wildcard and lets you `exclude("sub.path")` so only selected DSL + children end up under `namespace.command.*`. Enabled nodes automatically add their wildcard descendants + (e.g., `example.command.debug.*`) so granting the wildcard cascades to the remaining children. + +### Selective wildcards + +- **DSL** – call `wildcard { exclude("cooldown") }` to enable the `*. *` permission while skipping specific + literal/argument branches. You can chain `exclude` calls and pass multi-segment paths (`exclude("debug.logs")`). +- **Mutable tree** – after `wildcard = true`, invoke `excludeWildcardChild("helper")` (relative) or + `excludeWildcardChildAbsolute("example.command.helper.extras")` to trim wildcard membership imperatively. - **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 830de21..4405ddd 100644 --- a/src/main/kotlin/net/hareworks/permits_lib/domain/MutablePermissionTree.kt +++ b/src/main/kotlin/net/hareworks/permits_lib/domain/MutablePermissionTree.kt @@ -103,6 +103,18 @@ class MutablePermissionTree internal constructor( val newPermissionId = PermissionId.of("${this.id.value}.${newId.lowercase()}") renameSubtree(oldPermissionId, newPermissionId) } + + fun excludeWildcardChild(id: String) { + require(id.isNotBlank()) { "Wildcard exclusion id must not be blank." } + val permissionId = PermissionId.of("${this.id.value}.${id.lowercase()}") + draft.wildcardExclusions.add(permissionId) + } + + fun excludeWildcardChildAbsolute(id: String) { + require(id.isNotBlank()) { "Wildcard exclusion id must not be blank." } + val permissionId = PermissionId.of(id.lowercase()) + draft.wildcardExclusions.add(permissionId) + } } private fun removeSubtree(rootId: PermissionId) { @@ -150,7 +162,8 @@ class MutablePermissionTree internal constructor( defaultValue = draft.defaultValue, children = draft.children.toMutableMap(), wildcard = draft.wildcard, - registration = draft.registration + registration = draft.registration, + wildcardExclusions = draft.wildcardExclusions.toMutableSet() ) drafts[newId] = newDraft } 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 ad153a8..f20df75 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 wildcard: Boolean = false, - val registration: NodeRegistration = NodeRegistration.PERMISSION + val registration: NodeRegistration = NodeRegistration.PERMISSION, + val wildcardExclusions: Set = emptySet() ) { 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 72ecce0..f098558 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(), var wildcard: Boolean = false, - var registration: NodeRegistration = NodeRegistration.PERMISSION + var registration: NodeRegistration = NodeRegistration.PERMISSION, + val wildcardExclusions: MutableSet = linkedSetOf() ) { fun toNode(): PermissionNode = PermissionNode( @@ -17,7 +18,8 @@ internal data class PermissionNodeDraft( defaultValue = defaultValue, children = children.toMap(), wildcard = wildcard, - registration = registration + registration = registration, + wildcardExclusions = wildcardExclusions.toSet() ) companion object { @@ -28,7 +30,8 @@ internal data class PermissionNodeDraft( defaultValue = node.defaultValue, children = node.children.toMutableMap(), wildcard = node.wildcard, - registration = node.registration + registration = node.registration, + wildcardExclusions = node.wildcardExclusions.toMutableSet() ) } } diff --git a/src/main/kotlin/net/hareworks/permits_lib/domain/WildcardAugmentor.kt b/src/main/kotlin/net/hareworks/permits_lib/domain/WildcardAugmentor.kt index 1cf084d..71cfa42 100644 --- a/src/main/kotlin/net/hareworks/permits_lib/domain/WildcardAugmentor.kt +++ b/src/main/kotlin/net/hareworks/permits_lib/domain/WildcardAugmentor.kt @@ -12,7 +12,9 @@ internal object WildcardAugmentor { if (node.id.value.endsWith(".*")) return@forEach val wildcardId = PermissionId.of("${node.id.value}.*") - val updatedChildren = node.children.toMutableMap() + val updatedChildren = node.children + .filterKeys { childId -> childId !in node.wildcardExclusions } + .toMutableMap() val existing = result[wildcardId] if (existing == null) { 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 21f50a8..ecd2727 100644 --- a/src/main/kotlin/net/hareworks/permits_lib/dsl/PermissionNodeBuilder.kt +++ b/src/main/kotlin/net/hareworks/permits_lib/dsl/PermissionNodeBuilder.kt @@ -28,6 +28,11 @@ class PermissionNodeBuilder internal constructor( draft.wildcard = value } + fun wildcard(block: WildcardDsl.() -> Unit) { + wildcard = true + WildcardDsl(draft).apply(block) + } + var registration: NodeRegistration get() = draft.registration set(value) { @@ -66,3 +71,18 @@ class PermissionNodeBuilder internal constructor( treeBuilder.nestedNode(draft, id, registration, block) } } + +class WildcardDsl internal constructor( + private val draft: PermissionNodeDraft +) { + fun exclude(vararg segments: String) { + val normalized = segments + .flatMap { it.split('.') } + .map { it.trim().lowercase() } + .filter { it.isNotEmpty() } + if (normalized.isEmpty()) return + val suffix = normalized.joinToString(".") + val permissionId = PermissionId.of("${draft.id.value}.$suffix") + draft.wildcardExclusions.add(permissionId) + } +}