Permissions
A module can declare a permissions: tree — a nested set of access control
permissions the module contributes. Each permission has a key (its slug in the
generated identifier) and a name (a programmatic identifier the compiler uses to
name the generated Kotlin object for it, e.g. name: managePosts generates
object ManagePostsPermission). The identifier itself is either set explicitly via
fullKey: or computed by the compiler as {parent.fullKey}.{key} while the module
is preprocessed. If key is left empty too, it’s normalized from name (e.g.
name: managePosts with no key becomes key: managePosts).
name: blogpermissions: - key: post name: managePosts title: en: Manage posts description: en: Permissions related to blog posts children: - key: create name: createPost title: en: Create post - key: publish name: publishPost title: en: Publish postpost gets no explicit fullKey, so the compiler derives one: post. Its children
inherit that as their prefix, becoming post.create and post.publish. Because
post groups children and left its key for the compiler to derive, its generated
key gets a trailing .* — post.* — signaling it covers itself and everything below
it. A fullKey set explicitly is always used as-is, even on a node with children,
since the author chose that exact string on purpose.
Compiling this produces Permissions.kt:
- one exported
objectper root permission — not a single object wrapping the whole forest, and no separate flat constant per node either — so your IDE can autocomplete straight down it, e.g.ManagePostsPermission.CreatePost.key. A node with children is a nested Kotlinobject(Kotlin’s idiomatic compile-time singleton namespace): its ownkey/name/title/descriptionsit directly on it asvals, alongside one nested object/val per child (named by the child’s own identifier, PascalCase). A node with no children collapses to a plainvalholding aPermissioninstance directly. The self attributes are always lowercase while every child is always PascalCase, so the two can never collide — there’s no case-collision escape hatch to reach for, unlike a language that flattens both into the same casing convention. The root’s own name is always PascalCase with a trailingPermission(e.g.managePosts→ManagePostsPermission), so it reads unambiguously as a permission and can’t collide with an unrelated single-letter root. - alongside each root, a
<Name>PermissionList: List<Permission>: that root permission and every one of its descendants, flattened into its own list, by referencing each node’s ownpermissionval rather than re-declaring separate literals, so it can never drift out of sync with it. One list per root, not a single list combining every root — a module with several unrelated permission groups shouldn’t force every caller to filter one combined list back down to the group it actually cares about.
The Permission data class itself only holds a single node’s own attributes — no
children property — since the tree structure lives in the nested objects above,
not in the data instances; it exists purely as the common type every node’s
permission val and every <Name>PermissionList entry share.
data class Permission( val key: String, val name: String, val title: Map<String, String>? = null, val description: Map<String, String>? = null)
// ManagePostsPermission mirrors the "managePosts" permission node (and everything nested under it),// so it can be navigated directly, e.g. ManagePostsPermission.key.object ManagePostsPermission { val key: String = "post.*" val name: String = "managePosts" val title: Map<String, String>? = mapOf("en" to "Manage posts") val description: Map<String, String>? = mapOf("en" to "Permissions related to blog posts") val permission: Permission = Permission(key = key, name = name, title = title, description = description)
val CreatePost: Permission = Permission( key = "post.create", name = "createPost", title = mapOf("en" to "Create post"), description = null )
val PublishPost: Permission = Permission( key = "post.publish", name = "publishPost", title = mapOf("en" to "Publish post"), description = null )}
// ManagePostsPermissionList flattens ManagePostsPermission (and everything nested under it) into a single list,// for anything that wants to walk them all at once (seeding an ACL table,// rendering a permissions picker, ...).val ManagePostsPermissionList: List<Permission> = listOf( ManagePostsPermission.permission, ManagePostsPermission.CreatePost, ManagePostsPermission.PublishPost,)A module with a second, unrelated root permission (e.g. viewReports, with no
children) gets its own separate val ViewReportsPermission: Permission and its own
separate ViewReportsPermissionList right alongside it — not folded into
ManagePostsPermissionList.