Skip to content

refactor(model): add pure-Java domain value types with TypedString/StringId typed IDs#13

Merged
nbdSteve merged 16 commits into
masterfrom
fm/armorplus-4b-domaintypes-d5
Jul 9, 2026
Merged

refactor(model): add pure-Java domain value types with TypedString/StringId typed IDs#13
nbdSteve merged 16 commits into
masterfrom
fm/armorplus-4b-domaintypes-d5

Conversation

@nbdSteve

@nbdSteve nbdSteve commented Jul 9, 2026

Copy link
Copy Markdown
Owner

Intent

Refactor typed ID wrappers from standalone Lombok @value classes to a shared TypedString/StringId base-class hierarchy using commons-lang3 for equals/hashCode/validation. TypedString is the abstract base with null-rejecting constructor, HashCodeBuilder/EqualsBuilder (with getClass() check so different subtypes with same string are NOT equal), toString(), and a static toStrings() utility. StringId extends TypedString adding Validate.notEmpty. ArmorSetId, DamageCauseId, PotionEffectId, SoundId extend StringId with private constructor + of() factory. PlayerId extends TypedString directly (wraps UUID, stores UUID.toString() for the base, exposes getValue() for the original UUID). commons-lang3 3.17.0 added as compile dep, shaded to gg.steve.mc.ap.lib.commons.lang3. TypedString and StringId added to 100% JaCoCo core gate. The captain provided the exact TypedString source shape to implement faithfully. DamageCauseId is a bounded domain key (not free text), confirmed as correct by captain.

What Changed

  • Added a new gg.steve.mc.ap.model package of pure-Java domain value types (set, ability, combat, effect, notification, player sub-packages) built with Lombok @Value/@Builder and @Singular for immutable collections, with zero Bukkit/NMS/NBT imports.
  • Introduced typed identity wrappers (ArmorSetId, DamageCauseId, PotionEffectId, SoundId, PlayerId) on a shared TypedString/StringId base hierarchy: null/empty rejection via commons-lang3 Validate, getClass()-checked equals/hashCode so different subtypes wrapping the same string are never equal, final leaf classes with private constructors and of() factories; commons-lang3 3.17.0 is added as a compile dependency and shaded to gg.steve.mc.ap.lib.commons.lang3.
  • Extended the JaCoCo check-core 100% line+branch gate to cover TypedString and StringId, added unit tests for every new model type (185 tests total pass under mvn -B clean verify on JDK 25), gitignored build.log, and updated AGENTS.md/README.md conventions to match the new hierarchy.

Risk Assessment

✅ Low: Additive-only change introducing pure-Java value types with comprehensive test coverage and a 100% JaCoCo gate; no existing behavior is altered, and all previous review findings have been resolved.

Testing

Full mvn verify under JDK 25 confirms the TypedString/StringId hierarchy works as intended: all 185 tests pass green, the 100% coverage gate on the two abstract bases succeeds, commons-lang3 is fully shaded in the final plugin JAR, and cross-type inequality plus null/empty rejection are exercised by dedicated tests.

Evidence: Verification results (build, tests, JaCoCo, shading)
=== TypedString/StringId Refactor - Verification Results ===

1. BUILD + TESTS
   Command: JAVA_HOME=<jdk25> mvn -B clean verify
   Result: BUILD SUCCESS
   Tests:  185 run, 0 failures, 0 errors, 0 skipped

2. JACOCO CORE COVERAGE GATE (check-core, 100% line + branch required)
   TypedString: 100% instruction coverage, 100% branch coverage (6/6 branches, 12/12 lines)
   StringId:    100% instruction coverage, n/a branches (3/3 lines)
   Gate result: PASSED (build would fail otherwise)

3. COMMONS-LANG3 SHADING
   Shaded to:  gg/steve/mc/ap/lib/commons/lang3/ (413 classes in final JAR)
   Unshaded:   0 classes remaining at org/apache/commons/lang3/
   Conclusion: No classpath collision risk on Spigot server

4. KEY BEHAVIORAL TESTS (all passing)
   - differentSubtypesWithSameStringAreNotEqual: ArmorSetId("SPEED") != PotionEffectId("SPEED")
   - nullConstructorThrows: ArmorSetId.of(null) -> NullPointerException
   - sameTypeSameValueEqual: ArmorSetId("dragon") == ArmorSetId("dragon")
   - sameTypeDifferentValueNotEqual: ArmorSetId("dragon") != ArmorSetId("knight")
   - equalsNullReturnsFalse / equalsSelfReturnsTrue
   - StringId rejects empty string (StringIdTest)
   - PlayerId wraps UUID, rejects null (PlayerIdTest)

5. HIERARCHY STRUCTURE CONFIRMED
   TypedString (abstract) <- StringId (abstract) <- ArmorSetId, DamageCauseId, PotionEffectId, SoundId (final)
   TypedString (abstract) <- PlayerId (final, wraps UUID)
   All leaf classes: final + private constructor + static of() factory

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

🔧 **Review** - 1 issue found → auto-fixed (2) ✅
  • ℹ️ src/main/java/gg/steve/mc/ap/model/id/ArmorSetId.java:3 - The leaf ID classes (ArmorSetId, DamageCauseId, PotionEffectId, SoundId, PlayerId) are not declared final. The private constructors already prevent external subclassing, making them effectively final, but marking them final would communicate the design intent explicitly and prevent accidental internal subclassing within the same package.

🔧 Fix: Mark leaf ID classes final
4 issues (2 warnings, 2 infos) still open:

  • ⚠️ build.log:1 - Commit fbd5a11 re-commits the 146-line Maven build.log at the repo root, directly reverting the immediately preceding commit d766053 ("chore: remove accidental build log"). The file is a throwaway build artifact containing machine-specific absolute paths (/Users/goodhill/...). It should be deleted again, and adding a build.log entry to .gitignore would stop this recurring (the agent workflow routinely redirects build output to build.log).
  • ⚠️ AGENTS.md:15 - AGENTS.md line 15 still documents the old Lombok-based design: "ID wrappers enforce null safety via @nonnull on the value field; both the generated constructor and the of() factory reject null". Commit 341947a replaced that with the TypedString/StringId hierarchy where null rejection happens via commons-lang3 Validate.notNull in the base constructor and there is no @nonnull or generated constructor anymore. The convention note should be rewritten to describe the TypedString/StringId base-class approach so future agent sessions don't follow stale guidance.
  • ℹ️ AGENTS.md:8 - pom.xml adds gg.steve.mc.ap.model.id.TypedString and StringId to the JaCoCo check-core 100% gate, but the documented core-class lists in AGENTS.md line 8 and README.md line 15 were not updated to include them (they stop at ArmorHandItem). Future contributors modifying TypedString/StringId won't know the build enforces 100% coverage on them.
  • ℹ️ pom.xml:246 - commons-lang3 3.17.0 (~670 KB) is compile-scoped and shaded into the plugin jar to supply just three small utilities (Validate, EqualsBuilder, HashCodeBuilder). The relocation to gg.steve.mc.ap.lib.commons.lang3 is correct for avoiding server classpath clashes, but the whole library lands in the artifact since minimizeJar is not enabled. Acceptable tradeoff for a Spigot plugin; noting it in case jar size ever matters.

🔧 Fix: Delete build.log, gitignore it, update stale docs
✅ Re-checked - no issues remain.

✅ **Test** - passed

✅ No issues found.

  • mvn -B clean verify (JDK 25) - 185 tests, BUILD SUCCESS, JaCoCo check-core 100% line+branch on TypedString and StringId
  • Verified commons-lang3 shading: 413 classes at gg/steve/mc/ap/lib/commons/lang3/, 0 at org/apache/commons/lang3/ in final JAR
  • Confirmed cross-type inequality test (TypedStringTest.differentSubtypesWithSameStringAreNotEqual)
  • Confirmed null/empty rejection tests (TypedStringTest.nullConstructorThrows, StringIdTest, PlayerIdTest)
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

Stephen Goodhill added 5 commits July 9, 2026 15:58
Introduce `gg.steve.mc.ap.model` top-level package containing core
domain value types organized by concept sub-package (set, ability,
combat, effect, notification, player). All types are immutable value
carriers using Lombok @Value/@builder with zero Bukkit/NMS/NBT imports.

Types added:
- model.set: PieceSlot enum, PurchaseResult enum
- model.ability: AbilityType enum, BasicStats, HandItemSpec
- model.combat: DamageContext, DamageModification
- model.effect: EffectSpec, SoundSpec
- model.notification: Notification
- model.player: Wearer

HandItemSpec carries the pure calculateFinalDamage math (mirroring
HandSetData) and is added to the JaCoCo 100% core gate. All other
types have construction/equality unit tests.

Also adds:
- Lombok 1.18.38 dependency (provided scope) with annotation processor
- lombok.config enabling @generated annotation for JaCoCo exclusion
- AGENTS.md updated with model package conventions
Rename all domain value types to use explicit, unambiguous names per
project naming conventions:

- PieceSlot -> ArmorPieceSlot
- PurchaseResult -> ArmorSetPurchaseResult
- AbilityType -> ArmorSetAbilityType
- BasicStats -> ArmorSetBasicStats
- HandItemSpec -> ArmorHandItemSpec
- DamageContext -> CombatDamageContext
- DamageModification -> CombatDamageModification
- EffectSpec -> PotionEffectSpec
- SoundSpec -> NotificationSoundSpec
- Notification -> ArmorSetNotification
- Wearer -> ArmorSetWearer

Updates JaCoCo core gate include and AGENTS.md accordingly.
Use Lombok @Singular on messages and commands fields so the builder
produces unmodifiable defensive copies at build() time. Callers
retaining the original list can no longer mutate the notification
post-construction.

Tests added proving:
- Mutating the source list after build does not affect stored value
- getMessages()/getCommands() throw on modification attempts
- Empty lists returned (not null) when no items added
@nbdSteve nbdSteve changed the title feat(model): add pure-Java domain value types with Lombok feat(model): add pure-Java domain value types with Lombok and enforce immutability Jul 9, 2026
Rename PotionEffectSpec -> PotionEffect and NotificationSoundSpec ->
NotificationSound. The clean short name belongs to the domain model;
any future adapter code referencing the Bukkit PotionEffect uses the
fully-qualified org.bukkit.potion.PotionEffect at that boundary.
@nbdSteve nbdSteve changed the title feat(model): add pure-Java domain value types with Lombok and enforce immutability feat(model): add pure-Java domain value types with Lombok Jul 9, 2026
Consistent with earlier rename of effect types - the domain model uses
clean names without the Spec suffix. Updates JaCoCo core gate, README,
and AGENTS.md references.
@nbdSteve nbdSteve changed the title feat(model): add pure-Java domain value types with Lombok refactor(model): add pure-Java domain value types with Lombok Jul 9, 2026
Stephen Goodhill added 4 commits July 10, 2026 08:17
Add model.id sub-package with typed string/UUID wrappers to eliminate
primitive obsession for domain identity keys:
- PlayerId (wraps UUID)
- ArmorSetId (wraps String set name)
- DamageCauseId (wraps String damage cause key)
- PotionEffectId (wraps String effect type name)
- SoundId (wraps String sound name key)

Each provides a static of() factory. All model value types now use
these typed IDs instead of raw String/UUID for identity fields.
Free-text strings (messages, commands) remain unwrapped.

Also renames ArmorHandItemSpec -> ArmorHandItem (drops Spec suffix
consistent with prior effect type renames).
Add Lombok @nonnull to the value field in all typed ID wrappers so both
the of() factory and the generated constructor reject null. This closes
the bypass where new ArmorSetId(null) would silently succeed. The of()
factory now simply delegates to the constructor (Lombok handles the
null check). Update AGENTS.md to reference typed ID wrappers as the
identity convention.
@nbdSteve nbdSteve changed the title refactor(model): add pure-Java domain value types with Lombok feat(model): introduce pure-Java domain value types with typed ID wrappers Jul 9, 2026
Stephen Goodhill added 5 commits July 10, 2026 08:59
Replace per-wrapper Lombok @value IDs with a shared base-class
hierarchy using commons-lang3 for equals/hashCode/validation:

- TypedString: abstract base with final String id field, null-rejecting
  constructor (Validate.notNull), EqualsBuilder/HashCodeBuilder, and a
  static toStrings() utility
- StringId extends TypedString: adds Validate.notEmpty guard
- ArmorSetId, DamageCauseId, PotionEffectId, SoundId extend StringId
- PlayerId extends TypedString (stores UUID, exposes getValue())

Adds commons-lang3 3.17.0 as a compile dependency, shaded under
gg.steve.mc.ap.lib.commons.lang3. TypedString and StringId added to
the JaCoCo 100% core gate.
@sonarqubecloud

sonarqubecloud Bot commented Jul 9, 2026

Copy link
Copy Markdown

@nbdSteve nbdSteve changed the title feat(model): introduce pure-Java domain value types with typed ID wrappers refactor(model): add pure-Java domain value types with TypedString/StringId typed IDs Jul 9, 2026
@nbdSteve
nbdSteve merged commit 2f3ba3c into master Jul 9, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant