Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 43 additions & 37 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,71 +1,77 @@
# Tavall Java Utils
# Tavall Custom Enum Java

Low-level, dependency-light Java utility primitives shared across Tavall projects.
A small, dependency-light Java utility for defining strongly typed enum-like values with ordinary classes.

The repository is part of the Tavall Java Tools family but remains independently consumable and versioned. Utilities belong here only when they are broadly useful Java primitives with no stronger domain owner such as concurrency, reflection, DI, caching, logging, or persistence.
`CustomEnum` is intentionally low level. A custom enum declares its values once in its own class, then consumers use those typed constants exactly as they would use a normal Java enum.

## CustomEnum
## Usage

`CustomEnum` provides strongly typed, extensible domain identifiers for places where a Java `enum` is too closed but raw strings are too weak.

A custom-enum family declares one concrete type and its well-known values:
Define the enum type and register its values once:

```java
public final class ModuleProfile extends CustomEnum<ModuleProfile> {
private static final CustomEnumRegistry<ModuleProfile> VALUES =
CustomEnumRegistry.create("module-profile");
import org.tavall.util.CustomEnum;

public static final ModuleProfile KINGDOM = define("kingdom");
public static final ModuleProfile FFA = define("ffa");
public static final ModuleProfile LOBBY = define("lobby");
public final class ThreadType extends CustomEnum<ThreadType> {

private ModuleProfile(String id) {
super(id);
}
public static final ThreadType MAIN = register("MAIN");
public static final ThreadType AI = register("AI");
public static final ThreadType WORKER = register("WORKER");

public static ModuleProfile define(String id) {
return VALUES.register(new ModuleProfile(id));
private ThreadType(String name) {
super(name);
}

public static ModuleProfile fromId(String id) {
return VALUES.require(id);
private static ThreadType register(String name) {
return CustomEnum.register(new ThreadType(name));
}
}
```

Consumers then carry `ModuleProfile` instead of remembering magic strings:
Use it elsewhere just like an enum:

```java
ModuleProfile id();
ThreadType type = ThreadType.AI;

void createThread(ThreadType type) {
// ...
}

createThread(ThreadType.WORKER);
```

Extension modules may register additional values during bootstrap through the family-owned `define(...)` method. Once discovery is complete, the family can freeze its registry so late registrations fail rather than silently changing runtime behavior.
Java's normal type system keeps different custom-enum families separate:

```java
DatabaseType databaseType = DatabaseType.POSTGRES;

createThread(databaseType); // does not compile
```

### Migration aliases
### Enum-style operations

Aliases let old serialized/configured identifiers resolve to the new canonical value without preserving the old identity in production code:
Each registered value exposes its declared name and declaration-order ordinal:

```java
ModuleId tavallFfa = ModuleId.define("tavall-ffa");
ModuleId.registry().registerAlias("novus-ffa", tavallFfa);
ThreadType.AI.name(); // "AI"
ThreadType.AI.ordinal(); // 1
ThreadType.AI.toString();// "AI"
```

A lookup of `novus-ffa` returns the canonical `tavall-ffa` object. The canonical object's `id()` never changes.
Values can be looked up or enumerated through `CustomEnum`:

```java
ThreadType ai = CustomEnum.valueOf(ThreadType.class, "AI");
List<ThreadType> values = CustomEnum.values(ThreadType.class);
```

### Deliberate constraints
`valueOf(...)` is exact and throws for unknown names. `values(...)` returns an immutable snapshot in declaration order.

- A value is strongly typed by its concrete custom-enum family.
- Registries reject duplicate canonical IDs and aliases immediately.
- Aliases must point at the exact canonical object already owned by that registry.
- IDs are exact. The library does not silently lowercase, trim, or otherwise normalize wire/config values.
- There are no ordinal semantics because extension registration order is not a stable domain contract.
- Parsing strings belongs at configuration, serialization, command, or network boundaries. Internal APIs should pass the typed value.
- `CustomEnum` is not DI and does not discover classes or modules. A domain owns its registry and extension policy.
Like Java enums, registered custom-enum values use identity equality. A duplicate name within the same custom-enum type is rejected during registration.

## Coordinates

```text
org.tavall:tavall-java-utils
org.tavall:tavall-custom-enum-java
```

## Build
Expand Down
6 changes: 3 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ plugins {
}

group = "org.tavall"
extra["versionTagPrefix"] = "tavall-java-utils"
extra["versionTagPrefix"] = "tavall-custom-enum-java"
apply(from = "gradle/git-version.gradle.kts")
version = extra["gitVersion"] as String

Expand Down Expand Up @@ -40,15 +40,15 @@ publishing {
publications {
create<MavenPublication>("mavenJava") {
from(components["java"])
artifactId = "tavall-java-utils"
artifactId = "tavall-custom-enum-java"
}
}
repositories {
val token = providers.environmentVariable("GITHUB_TOKEN")
if (token.isPresent) {
maven {
name = "GitHubPackages"
url = uri("https://maven.pkg.github.com/TavallStudios/tavall-java-utils")
url = uri("https://maven.pkg.github.com/TavallStudios/tavall-custom-enum-java")
credentials {
username = providers.environmentVariable("GITHUB_ACTOR").orNull
password = token.get()
Expand Down
2 changes: 1 addition & 1 deletion settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ pluginManagement {
}
}

rootProject.name = "tavall-java-utils"
rootProject.name = "tavall-custom-enum-java"
101 changes: 80 additions & 21 deletions src/main/java/org/tavall/util/CustomEnum.java
Original file line number Diff line number Diff line change
@@ -1,55 +1,114 @@
package org.tavall.util;

import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
* Base type for extensible, enum-like domain identifiers.
* Base type for strongly typed, enum-like values declared with ordinary Java classes.
*
* <p>A {@code CustomEnum} value is immutable and strongly typed by its concrete subclass.
* Values are normally declared and canonicalized through a {@link CustomEnumRegistry}. Unlike
* a Java {@code enum}, extension modules may register additional values during bootstrap.</p>
* <p>Each concrete subclass declares its values once by calling {@link #register(CustomEnum)}.
* Registered values are unique by name within their concrete type, preserve declaration order,
* expose enum-style names and ordinals, and use identity equality just like Java enums.</p>
*
* @param <T> concrete custom-enum family
* @param <T> concrete custom-enum type
*/
public abstract class CustomEnum<T extends CustomEnum<T>> {

private final String id;
private static final Map<Class<?>, EnumValues<?>> VALUES = new ConcurrentHashMap<>();

protected CustomEnum(String id) {
this.id = requireIdentifier(id, "id");
private final String name;
private int ordinal = -1;

protected CustomEnum(String name) {
this.name = requireName(name);
}

/** Returns the declared name of this value. */
public final String name() {
return name;
}

/** Returns the declaration-order ordinal of this value. */
public final int ordinal() {
if (ordinal < 0) {
throw new IllegalStateException("Custom enum value is not registered: " + name);
}
return ordinal;
}

/**
* Registers one value for its concrete custom-enum type and returns that same instance.
* Duplicate names within the same type are rejected immediately.
*/
protected static <T extends CustomEnum<T>> T register(T value) {
Objects.requireNonNull(value, "value");

@SuppressWarnings("unchecked")
Class<T> type = (Class<T>) value.getClass();

return valuesFor(type).register(value);
}

/** Returns the stable serialized identifier for this value. */
public final String id() {
return id;
/** Returns the registered value named {@code name} for {@code type}. */
public static <T extends CustomEnum<T>> T valueOf(Class<T> type, String name) {
Objects.requireNonNull(type, "type");
Objects.requireNonNull(name, "name");
ensureInitialized(type);
return valuesFor(type).valueOf(name);
}

/** Returns the registered values for {@code type} in declaration order. */
public static <T extends CustomEnum<T>> List<T> values(Class<T> type) {
Objects.requireNonNull(type, "type");
ensureInitialized(type);
return valuesFor(type).values();
}

final void assignOrdinal(int ordinal) {
if (this.ordinal >= 0) {
throw new IllegalStateException("Custom enum value is already registered: " + name);
}
this.ordinal = ordinal;
}

@Override
public final String toString() {
return id;
return name;
}

@Override
public final boolean equals(Object other) {
return this == other
|| (other != null
&& getClass() == other.getClass()
&& id.equals(((CustomEnum<?>) other).id));
return this == other;
}

@Override
public final int hashCode() {
return Objects.hash(getClass(), id);
return System.identityHashCode(this);
}

static String requireIdentifier(String value, String name) {
Objects.requireNonNull(value, name);
static String requireName(String value) {
Objects.requireNonNull(value, "name");
if (value.isBlank()) {
throw new IllegalArgumentException(name + " must not be blank");
throw new IllegalArgumentException("name must not be blank");
}
if (!value.equals(value.trim())) {
throw new IllegalArgumentException(name + " must not have leading or trailing whitespace");
throw new IllegalArgumentException("name must not have leading or trailing whitespace");
}
return value;
}

@SuppressWarnings("unchecked")
private static <T extends CustomEnum<T>> EnumValues<T> valuesFor(Class<T> type) {
return (EnumValues<T>) VALUES.computeIfAbsent(type, ignored -> new EnumValues<>(type));
}

private static void ensureInitialized(Class<?> type) {
try {
Class.forName(type.getName(), true, type.getClassLoader());
} catch (ClassNotFoundException exception) {
throw new IllegalStateException("Unable to initialize custom enum type: " + type.getName(), exception);
}
}
}
Loading
Loading