HivePaaS
An open-source, self-hosted PaaS on Docker Swarm. Deploy apps, databases and functions from one VPS to a whole cluster, with SSL, backups and autoscaling built in.
Kotlin (K2) compiler plugin that generates enum-like operations (entries / valueOf / label) for sealed class hierarchies
Developer toolsKotlin · JavaGitHub @ProjectMapKOnlineOwner unconfirmed
Have you tried it? One tap is enough. No sign-in needed.
Owners of public repositories under a personal GitHub account can sign in to verify ownership and manage the introduction themselves. No administrator approval is needed.
Sign in, generate a verification file, and commit it to this repository’s default branch within 30 minutes. Files on other branches or forks do not count. Then check the file below to manage the project without administrator approval. Keep the file to retain your access.
If needed, you will go to sign-in. After signing in, return here and press your chosen verification button again.
Run /nomorevibe verify in your project folder to verify ownership and manage the introduction yourself.
Verify in one minuteAsk about other verification options
The maker's introduction consists of the line above. Read the repository README for more details. Repository README ↗
Loading the full README.
Read the full README ↗A Kotlin (K2) compiler plugin that generates enum-like operations — entries, valueOf, label
and friends — for sealed class / sealed interface hierarchies at compile time.
Hierarchies keep their expressive power — data-carrying entries, exhaustive when with smart
casts, open leaves — and gain the operational API of enums on top.
No runtime reflection involved, working on all Kotlin Multiplatform targets.
Published on the
Gradle Plugin Portal,
so plugins {} resolves it without extra repository configuration:
plugins {
kotlin("jvm") version "2.4.21" // or kotlin("multiplatform") — any Kotlin target plugin
id("io.github.projectmapk.sealed-class-enumizer") version "2.4.21-0.1.3"
}
The Gradle plugin applies the compiler plugin to every compilation and automatically adds the
runtime API (io.github.projectmapk:sealed-class-enumizer-runtime-api) — with api scope for
production code, because the generated API exposes runtime API types as supertypes.
sealedClassEnumizer {
addRuntimeDependency = true // set false to declare the runtime API manually
labelCase = LabelCase.AS_DECLARED // project-wide default label case (see Label customization)
}
Declare the plugin as a dependency of kotlin-maven-plugin and name it in compilerPlugins; the
compiler plugin follows as a transitive dependency. It applies to every kotlin-maven-plugin
execution, production and test compilations alike.
<plugin>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-plugin</artifactId>
<version>${kotlin.version}</version>
<configuration>
<compilerPlugins>
<plugin>sealed-class-enumizer</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>io.github.projectmapk</groupId>
<artifactId>sealed-class-enumizer-maven-plugin</artifactId>
<version>2.4.21-0.1.3</version>
</dependency>
</dependencies>
</plugin>
Unlike the Gradle plugin, the runtime API is not added for you — declare it as a project
dependency (Maven resolves the platform artifact, hence the -jvm suffix):
<dependency>
<groupId>io.github.projectmapk</groupId>
<artifactId>sealed-class-enumizer-runtime-api-jvm</artifactId>
<version>2.4.21-0.1.3</version>
</dependency>
The project-wide default label case is a property:
<properties>
<sealed-class-enumizer.labelCase>AS_DECLARED</sealed-class-enumizer.labelCase>
</properties>
The same option can be given through kotlin-maven-plugin's pluginOptions
(sealed-class-enumizer:labelCase=...), which takes precedence over the property.
IntelliJ's K2 mode does not load third-party compiler plugins by default, so generated
declarations show as unresolved in the editor even though Gradle builds succeed
(KTIJ-29248).
To make resolution and completion work, disable the registry flag
kotlin.k2.only.bundled.compiler.plugins.enabled (Help | Find Action… | "Registry…"), then
re-sync the project:
This IDE capability is experimental; command-line and Gradle builds are unaffected either way.
Once the flag is disabled, IntelliJ resolves the generated declarations and renders them inline:
import io.github.projectmapk.sealedClassEnumizer.Enumize
import io.github.projectmapk.sealedClassEnumizer.label
@Enumize
sealed interface SI {
data class Foo(val v: Int) : SI
data object Bar : SI
}
// enum-like operations; one singleton ("kind") per leaf
SI.Enumish.entries // [Bar, Foo] — compiler-provided order
SI.Enumish.valueOf("Foo") // label-based lookup; IllegalArgumentException when absent
SI.Enumish.valueOfOrNull("X") // null-returning variant (an addition over enums)
SI.Enumish.entries.map { it.enumizedClass } // [Bar::class, Foo::class] — all leaf classes
// from a value to its kind
val si: SI = SI.Foo(42)
si.asEnumish() // Foo's kind (= Foo.Companion)
si.label // "Foo" — enum's `name` counterpart
si.asEnumish().enumizedClass // SI.Foo::class — the bridge to reflection-based libraries
// exhaustive when over kinds (the generated Enumish is sealed — no else branch needed)
when (si.asEnumish()) {
SI.Foo -> println("a Foo")
SI.Bar -> println("a Bar")
}
Notes:
entries order is the compiler-provided inheritor order (FQN-based), not declaration order.label or a custom property instead.entries.map { it.enumizedClass } lists every leaf class without reflection —
KClass.sealedSubclasses needs the JVM and kotlin-reflect, and silently breaks under R8
(KT-25871,
KT-37292).ordinal and Comparable are deliberately not provided: such numbers change on renames and
must not be persisted.open / abstract class, interface, fun interface): subtypes defined
outside the hierarchy are absorbed into the leaf's kind, so entries stays fixed while
implementations remain extensible.label property, the extension is shadowed (the plugin warns about it) —
asEnumish().label is always reliable.A leaf that carries data has no instance until the data exists, so on a plain sealed hierarchy
"which status" cannot appear in a signature.
The usual escape hatches — fabricating a throwaway Status from dummy data, or defining a
parallel enum that must be kept in sync — are exactly what kinds eliminate: they are
always-available singletons, accepted wherever an enum would have been:
@Enumize
sealed interface Status {
data class Active(val remarks: String) : Status
data class Suspended(val remarks: String) : Status
data object Deleted : Status
}
class Foo(val status: Status)
interface FooRepository {
fun searchFoo(vararg statuses: Status.Enumish): List<Foo>
}
class InMemoryFooRepository(private val foos: List<Foo>) : FooRepository {
override fun searchFoo(vararg statuses: Status.Enumish): List<Foo> =
foos.filter { it.status.asEnumish() in statuses }
}
// call sites read like enums — no Status instance is fabricated or fetched;
// a data class's kind (Active) and a data object (Deleted) pass uniformly
repository.searchFoo(Status.Active, Status.Deleted)
This automates the hand-written workaround of giving every leaf a companion that implements a shared marker interface (background article, Japanese).
Data-carrying leaves have no instances to enumerate, so "all statuses" for a picker or a report
axis traditionally means a hand-maintained list — one that silently goes stale when a leaf is
added.
entries is compiler-generated and complete by construction:
// a filter UI offering every status — new leaves show up without touching this code
val statusOptions: List<String> = Status.Enumish.entries.map { it.label }
// aggregation axes that keep empty groups (groupBy alone would drop them)
val fooCountByStatus: Map<Status.Enumish, Int> =
Status.Enumish.entries.associateWith { 0 } +
foos.groupingBy { it.status.asEnumish() }.eachCount()
Persisting "which case" — a DB column, a query parameter, an analytics event — normally takes a
hand-written string mapping that must follow the hierarchy.
label / valueOf are that mapping, generated; unlike value::class.simpleName, labels are
compile-time constants — never null and unaffected by R8 / minification renaming:
// outbound: labels are the wire form
fun statusQuery(selection: Set<Status.Enumish>): String =
selection.joinToString("&") { "status=${it.label}" }
// inbound: GET /foos?status=Active&status=Deleted — parsed kinds feed searchFoo from above
fun handle(rawStatuses: List<String>): List<Foo> {
val statuses = rawStatuses.map {
requireNotNull(Status.Enumish.valueOfOrNull(it)) { "unknown status: $it" }
}
return repository.searchFoo(*statuses.toTypedArray())
}
@EnumishLabel keeps persisted labels stable across leaf renames — see
Label customization.
Not all per-kind wiring fits an exhaustive when — handler registries assembled by DI or icon
sets contributed by feature modules live in data, where the compiler cannot check completeness.
entries turns "one per leaf" into a single assertion:
// the map is assembled elsewhere — no single `when` site exists
class StatusRenderer(private val cells: Map<Status.Enumish, CellRenderer>) {
init {
val missing = Status.Enumish.entries - cells.keys
require(missing.isEmpty()) { "statuses without a renderer: ${missing.map { it.label }}" }
}
}
JUnit's @EnumSource has no sealed counterpart, and community substitutes build on
sealedSubclasses — JVM-only reflection again.
entries drives a test over every leaf on any target, and enumizedClass states the expected
type of a value obtained through a kind:
// production code: a per-kind factory (form defaults, DB seeding, fixtures, …)
fun defaultStatusOf(kind: Status.Enumish): Status =
when (kind) {
Status.Active -> Status.Active(remarks = "")
Status.Suspended -> Status.Suspended(remarks = "payment failed")
Status.Deleted -> Status.Deleted
}
class DefaultStatusTest {
@Test
fun `every status yields a default of its own type`() {
for (kind in Status.Enumish.entries) {
assertEquals(kind.enumizedClass, defaultStatusOf(kind)::class)
}
}
}
A new leaf fails the factory's when at compile time; a branch fabricating the wrong case fails
the enumizedClass assertion.
Values of an open leaf's absorbed subtypes report their runtime class via ::class — compare
kinds instead (assertEquals(kind, value.asEnumish())) in such hierarchies.
Conceptually the plugin generates the following (in compiler internals — no source files are produced):
sealed interface SI : Enumized<SI.Enumish> {
sealed interface Enumish : io.github.projectmapk.sealedClassEnumizer.Enumish {
companion object : EnumishCompanion<Enumish> {
val entries: List<Enumish>
fun valueOf(value: String): Enumish
fun valueOfOrNull(value: String): Enumish?
}
}
// and per leaf: asEnumish(), plus the kind singleton —
// the leaf object itself, or the leaf class's companion object
}
Modules that merely consume a library built with this plugin do not need the plugin: the generated
API is regular metadata, resolvable from any module, including exhaustive when checks.
Labels default to the leaf's simple name and resolve with this priority:
@EnumishLabel("...") on the leaf — used as-is (no case conversion); for resolving label
clashes and keeping persisted labels stable across renames@Enumize(labelCase = ...) on the hierarchysealedClassEnumizer { labelCase }
or the Maven sealed-class-enumizer.labelCase propertyAS_DECLARED (no conversion)Available cases: AS_DECLARED, UPPER_SNAKE_CASE, SNAKE_CASE, KEBAB_CASE.
Word splitting matches kotlinx.serialization's JsonNamingStrategy (HTTPServer →
HTTP_SERVER), is locale-independent, and conversion results are frozen across releases because
labels are meant to be persisted.
Label uniqueness within a hierarchy is enforced at compile time.
| Kotlin | 2.4.x, K2 only. The plugin version encodes the Kotlin minor it targets; applying it to another Kotlin minor produces a build warning |
| Version format | <KotlinVersion>-<pluginVersion> |
| Build environment | Gradle 9+ or Maven 3.9+ / JDK 17+ |
| runtime-api (JVM) | Java 8+ bytecode |
| runtime-api targets | jvm, js, wasmJs, wasmWasi, linuxX64, linuxArm64, mingwX64, macosX64, macosArm64, iosArm64, iosSimulatorArm64, iosX64, androidNativeArm32, androidNativeArm64, androidNativeX64, androidNativeX86 |
typealias that points to a generated Enumish cannot be used as the first supertype of the
@Enumize base (a language-side constraint; usage as a type argument or on leaves is fine).@Enumize cannot be applied to expect / actual declarations.ordinal / Comparable are not provided by design (see Usage notes).<KotlinVersion>-<pluginVersion>.Apache License 2.0 — see LICENSE.
Design documents (Japanese) live under docs/.