feat: wildcard excludeの実装

This commit is contained in:
Keisuke Hirata 2025-12-05 01:01:13 +09:00
parent 2275cd9993
commit 660f9a3436
6 changed files with 61 additions and 13 deletions

View File

@ -18,7 +18,9 @@ class ExamplePlugin : JavaPlugin() {
node("command", NodeRegistration.STRUCTURAL) { node("command", NodeRegistration.STRUCTURAL) {
description = "Access to all example commands" description = "Access to all example commands"
defaultValue = PermissionDefault.OP defaultValue = PermissionDefault.OP
wildcard = true // create example.command.* wildcard {
exclude("cooldown") // example.command.* will skip cooldown
}
node("reload", NodeRegistration.PERMISSION) { node("reload", NodeRegistration.PERMISSION) {
description = "Allows /example reload (permission example.command.reload)" description = "Allows /example reload (permission example.command.reload)"
@ -32,7 +34,6 @@ class ExamplePlugin : JavaPlugin() {
node("cooldown", NodeRegistration.PERMISSION) { node("cooldown", NodeRegistration.PERMISSION) {
description = "Allows /example cooldown tweaks" 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: // The tree above materializes as permissions such as:
// example.command, example.command.reload, example.command.helper, example.command.cooldown, // example.command, example.command.reload, example.command.helper, example.command.cooldown,
// example.tools.repair, // 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). // export to plugin.yml or inspect Bukkit's /permissions output).
configureRuntimePermissions() configureRuntimePermissions()
@ -96,12 +97,12 @@ val baseTree = permissionTree("example") {
val mutable = MutablePermissionTree.from(baseTree) val mutable = MutablePermissionTree.from(baseTree)
mutable.node("command", NodeRegistration.STRUCTURAL) { mutable.node("command", NodeRegistration.STRUCTURAL) {
wildcard = true wildcard = true
excludeWildcardChild("helper") // keep helper out of command.*
node("debug", NodeRegistration.PERMISSION) { node("debug", NodeRegistration.PERMISSION) {
description = "Allows /example debug" description = "Allows /example debug"
defaultValue = PermissionDefault.OP defaultValue = PermissionDefault.OP
wildcard = true wildcard = true
} }
child("helper", value = false) // unlink helper if present
} }
mutable.removeNode("command.legacy") mutable.removeNode("command.legacy")
@ -130,9 +131,17 @@ stage edits procedurally before ever touching `MutationSession`.
other namespaces. other namespaces.
- **PermissionRegistry** calculates a diff between snapshots and performs the minimum additions, - **PermissionRegistry** calculates a diff between snapshots and performs the minimum additions,
removals, or updates via Bukkit's `PluginManager`. removals, or updates via Bukkit's `PluginManager`.
- **Wildcards** disabled by default; opt in by setting `wildcard = true` on any permission you want pulled - **Wildcards** disabled by default; opt in via `wildcard = true` or the richer `wildcard { ... }` block.
into its parent `namespace.command.*`. Enabled nodes automatically add their wildcard descendants (e.g., The block automatically enables the wildcard and lets you `exclude("sub.path")` so only selected DSL
`example.command.debug.*`) so granting a parent wildcard cascades through the tree. 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 - **Mutable edits** `permits.edit { ... }` clones the currently registered tree, lets you mutate nodes
imperatively, re-validates, and only pushes the structural diff to Bukkit. imperatively, re-validates, and only pushes the structural diff to Bukkit.
- **AttachmentSynchronizer** manages identity-based `PermissionAttachment`s and exposes high-level - **AttachmentSynchronizer** manages identity-based `PermissionAttachment`s and exposes high-level

View File

@ -103,6 +103,18 @@ class MutablePermissionTree internal constructor(
val newPermissionId = PermissionId.of("${this.id.value}.${newId.lowercase()}") val newPermissionId = PermissionId.of("${this.id.value}.${newId.lowercase()}")
renameSubtree(oldPermissionId, newPermissionId) 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) { private fun removeSubtree(rootId: PermissionId) {
@ -150,7 +162,8 @@ class MutablePermissionTree internal constructor(
defaultValue = draft.defaultValue, defaultValue = draft.defaultValue,
children = draft.children.toMutableMap(), children = draft.children.toMutableMap(),
wildcard = draft.wildcard, wildcard = draft.wildcard,
registration = draft.registration registration = draft.registration,
wildcardExclusions = draft.wildcardExclusions.toMutableSet()
) )
drafts[newId] = newDraft drafts[newId] = newDraft
} }

View File

@ -14,7 +14,8 @@ data class PermissionNode(
val defaultValue: PermissionDefault = PermissionDefault.FALSE, val defaultValue: PermissionDefault = PermissionDefault.FALSE,
val children: Map<PermissionId, Boolean> = emptyMap(), val children: Map<PermissionId, Boolean> = emptyMap(),
val wildcard: Boolean = false, val wildcard: Boolean = false,
val registration: NodeRegistration = NodeRegistration.PERMISSION val registration: NodeRegistration = NodeRegistration.PERMISSION,
val wildcardExclusions: Set<PermissionId> = emptySet()
) { ) {
init { init {
require(children.keys.none { it == id }) { "Permission node cannot be a child of itself." } require(children.keys.none { it == id }) { "Permission node cannot be a child of itself." }

View File

@ -8,7 +8,8 @@ internal data class PermissionNodeDraft(
var defaultValue: PermissionDefault = PermissionDefault.FALSE, var defaultValue: PermissionDefault = PermissionDefault.FALSE,
val children: MutableMap<PermissionId, Boolean> = linkedMapOf(), val children: MutableMap<PermissionId, Boolean> = linkedMapOf(),
var wildcard: Boolean = false, var wildcard: Boolean = false,
var registration: NodeRegistration = NodeRegistration.PERMISSION var registration: NodeRegistration = NodeRegistration.PERMISSION,
val wildcardExclusions: MutableSet<PermissionId> = linkedSetOf()
) { ) {
fun toNode(): PermissionNode = fun toNode(): PermissionNode =
PermissionNode( PermissionNode(
@ -17,7 +18,8 @@ internal data class PermissionNodeDraft(
defaultValue = defaultValue, defaultValue = defaultValue,
children = children.toMap(), children = children.toMap(),
wildcard = wildcard, wildcard = wildcard,
registration = registration registration = registration,
wildcardExclusions = wildcardExclusions.toSet()
) )
companion object { companion object {
@ -28,7 +30,8 @@ internal data class PermissionNodeDraft(
defaultValue = node.defaultValue, defaultValue = node.defaultValue,
children = node.children.toMutableMap(), children = node.children.toMutableMap(),
wildcard = node.wildcard, wildcard = node.wildcard,
registration = node.registration registration = node.registration,
wildcardExclusions = node.wildcardExclusions.toMutableSet()
) )
} }
} }

View File

@ -12,7 +12,9 @@ internal object WildcardAugmentor {
if (node.id.value.endsWith(".*")) return@forEach if (node.id.value.endsWith(".*")) return@forEach
val wildcardId = PermissionId.of("${node.id.value}.*") 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] val existing = result[wildcardId]
if (existing == null) { if (existing == null) {

View File

@ -28,6 +28,11 @@ class PermissionNodeBuilder internal constructor(
draft.wildcard = value draft.wildcard = value
} }
fun wildcard(block: WildcardDsl.() -> Unit) {
wildcard = true
WildcardDsl(draft).apply(block)
}
var registration: NodeRegistration var registration: NodeRegistration
get() = draft.registration get() = draft.registration
set(value) { set(value) {
@ -66,3 +71,18 @@ class PermissionNodeBuilder internal constructor(
treeBuilder.nestedNode(draft, id, registration, block) 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)
}
}