Releases: Traqueur-dev/Structura
1.6.0
🔧 Structura v1.6.0 – Migration to GroupeZ Repository & DateTime Support
Streamlined publishing infrastructure and enhanced temporal type support.
✨ New Features
📅 LocalDateTime Support
Added native support for LocalDateTime conversion, complementing the existing LocalDate functionality.
public record Event(
LocalDate date,
LocalDateTime timestamp // ✨ Now supported!
) implements Loadable {}
YAML:
date: 2023-10-05
timestamp: 2023-10-05T14:30:00
- Automatic parsing of ISO-8601 datetime strings
- Seamless integration with existing temporal type handling
- Full bidirectional conversion support
🏗️ Infrastructure Changes
🔄 Migration to GroupeZ Maven Repository
Structura now publishes to the official GroupeZ Maven repository, replacing JitPack for more reliable and faster artifact distribution.
New Repository Configuration:
repositories { maven { url = "https://repo.groupez.dev/releases" // For stable releases // Use "snapshots" instead of "releases" for development versions } }
dependencies {
implementation("fr.traqueur:structura:<VERSION>") // New coordinates
implementation("org.yaml:snakeyaml:2.4")
}
Old Configuration (Deprecated):
repositories { maven { url = "https://jitpack.io" } // ❌ No longer used }
dependencies {
implementation("com.github.Traqueur-dev:Structura:<VERSION>") // ❌ Deprecated
}
⚙️ Build System Enhancements
GitHub Actions Integration
- New CI/CD workflow:
.github/workflows/build.yml - Automated builds: On push to
mainanddevelopbranches - Pull request validation: Builds on PR events (opened, synchronized, reopened)
- Manual triggers: Via
workflow_dispatch - Automatic publishing: To GroupeZ Maven on successful builds
Workflow Features:
- Concurrency control (cancels in-progress builds)
- Reusable workflow from
GroupeZ-dev/actions - Maven credential management
- Discord notifications support (optional)
Gradle Configuration Improvements
- Shadow JAR: Integrated for uber-jar generation
- Target folder: Build outputs now go to
target/directory - SHA versioning: Support for Git commit-based versioning
- Custom classifiers: Archive classifier support for variant builds
- GroupeZ publish plugin: Simplified publishing with
re.alwyn974.groupez.publishv1.0.0
Gradle Wrapper Update
- Version: 8.10 → 8.14
- Latest features and bug fixes
- Improved build performance
🔧 Technical Details
ValueConverter Changes
// Added LocalDateTime formatter private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
// New conversion logic
if (targetType == LocalDateTime.class) {
return LocalDateTime.parse(value.toString(), DATE_TIME_FORMATTER);
}
Build Configuration
plugins { id("java-library") id("re.alwyn974.groupez.publish") version "1.0.0" // New id("com.gradleup.shadow") version "9.0.0-beta11" // New }group = "fr.traqueur"
version = property("version") as String// SHA-based versioning support
rootProject.extra.properties["sha"]?.let { sha ->
version = sha
}// Shadow JAR configuration
tasks.shadowJar {
archiveClassifier.set("")
destinationDirectory.set(rootProject.extra["targetFolder"] as File)
}tasks.build {
dependsOn(tasks.shadowJar)
}
// Publish configuration
publishConfig {
githubOwner = "Traqueur-dev"
useRootProjectName = true
}
Settings Configuration
pluginManagement {
repositories {
maven {
name = "groupezReleases"
url = uri("https://repo.groupez.dev/releases")
}
gradlePluginPortal()
}
}
🧪 Testing
New Test: LocalDateTime Conversion
@Test
@DisplayName("Should handle LocalDateTime conversion from string")
void shouldHandleLocalDateTimeConversion() {
var localDate = LocalDateTime.of(2023, 10, 5, 14, 30, 0);
assertEquals(localDate, valueConverter.convert("2023-10-05T14:30:00", LocalDateTime.class, "test"));
}
📋 Migration Guide
For Existing Users
Step 1: Update your repository configuration
Replace:
repositories {
maven { url = "https://jitpack.io" }
}
With:
repositories {
maven { url = "https://repo.groupez.dev/releases" }
}
Step 2: Update your dependency coordinates
Replace:
implementation("com.github.Traqueur-dev:Structura:<VERSION>")
With:
implementation("fr.traqueur:structura:<VERSION>")
Step 3: (Optional) Use LocalDateTime
If you need timestamp precision beyond date-only values:
// Before public record Event(LocalDate timestamp) implements Loadable {}
// After
public record Event(LocalDateTime timestamp) implements Loadable {}
# Before
timestamp: 2023-10-05
# After
timestamp: 2023-10-05T14:30:00
📊 Comparison: Repository Hosting
| Feature | JitPack (Old) | GroupeZ Maven (New) |
|---|---|---|
| Build Speed | Slow (on-demand) | Fast (pre-built) |
| Reliability | Variable | High |
| Cache Performance | Poor | Excellent |
| Custom Versioning | Limited | Full support |
| CI/CD Integration | Manual | Automated |
| Snapshot Support | Basic | Native |
🎯 Benefits
For Users
- ✅ Faster downloads: Pre-built artifacts with better CDN distribution
- ✅ Better reliability: Dedicated infrastructure with guaranteed uptime
- ✅ Snapshot support: Access to development versions via snapshots repository
- ✅ Enhanced DateTime: More precise temporal data with
LocalDateTime
For Contributors
- ✅ Automated publishing: No manual release process
- ✅ Better testing: Integrated CI/CD with PR validation
- ✅ Version management: Git SHA-based versioning for development builds
- ✅ Simplified workflows: Reusable GitHub Actions
⚠️ Breaking Changes
Repository Migration
- Old URL:
https://jitpack.io→ No longer maintained - Old coordinates:
com.github.Traqueur-dev:Structura→ Deprecated - New URL:
https://repo.groupez.dev/releases - New coordinates:
fr.traqueur:structura
Note: Version 1.6.0 is the first version available on GroupeZ Maven. Previous versions remain on JitPack but won't receive updates.
📝 Version
1.5.0 → 1.6.0
Changes:
- ✨ Added
LocalDateTimesupport with ISO-8601 parsing - 🏗️ Migrated to GroupeZ Maven repository
- 🔄 Updated dependency coordinates (
fr.traqueur:structura) - ⚙️ Added GitHub Actions CI/CD workflow
- 📦 Integrated Shadow plugin for uber-JAR generation
- 🔧 Gradle 8.10 → 8.14
- 🔧 Added GroupeZ publish plugin
- 📂 Build outputs now go to
target/directory - 🧪 New test for
LocalDateTimeconversion - 📖 Updated README with new repository URLs
Full Changelog: https://github.com/Traqueur-dev/Structura/compare/1.5.0...1.6.0
🚀 Getting Started
Quick Start with v1.6.0
build.gradle.kts:
repositories { mavenCentral() maven { url = uri("https://repo.groupez.dev/releases") } }
dependencies {
implementation("fr.traqueur:structura:1.6.0")
implementation("org.yaml:snakeyaml:2.4")
}
Example Usage:
public record ScheduledTask(
String name,
LocalDateTime scheduledAt,
LocalDate deadline
) implements Loadable {}
tasks:
- name: "Database...1.5.0
1.4.0
🔧 Structura v1.4.0 – Key-Based Polymorphic Discriminators
Use YAML keys as type discriminators for cleaner polymorphic configurations.
✨ New Feature
🎯 Key-as-Discriminator Mode for Polymorphic Types
Introducing @ Polymorphic(useKey = true) - a new mode that uses YAML map keys as discriminator values, eliminating redundant type fields and creating more intuitive configuration syntax.
@ Polymorphic(key = "type", useKey = true, inline = true) public interface ItemMetadata extends Loadable {}
public record FoodMetadata(int nutrition, double saturation) implements ItemMetadata {}
public record PotionMetadata(String color, String basePotionType) implements ItemMetadata {}
public record TrimMetadata(String material, String pattern) implements ItemMetadata {}
YAML - Traditional Mode (useKey = false):
metadata:
- type: food # Explicit discriminator
nutrition: 8
- type: potion # Explicit discriminator
color: "#FF0000"
YAML - Key-as-Discriminator Mode (useKey = true):
metadata:
food: # Key IS the discriminator
nutrition: 8
potion: # Key IS the discriminator
color: "#FF0000"
🎮 Use Cases
1. Simple Fields
public record Config(ItemMetadata trim) implements Loadable {}
trim: # "trim" becomes the discriminator value
material: DIAMOND
pattern: VEX
2. Collections (Lists)
metadata:
food: # Each key identifies the type
nutrition: 8
saturation: 9.6
potion:
color: "#00FF00"
base-potion-type: HEALING
3. Maps
public record Config(Map<String, ItemMetadata> namedItems) implements Loadable {}
named-items:
food: # Key serves as both map key AND discriminator
nutrition: 10
saturation: 12.0
trim:
material: NETHERITE
pattern: SILENCE
🔧 Implementation Details
Polymorphic Annotation Enhancement
- New parameter:
boolean useKey() default false- Enables key-based discriminator mode - Comprehensive JavaDoc: Detailed examples for simple fields, collections, and maps
ValueConverter Changes
- Enhanced:
convertCollection()- DetectsuseKeymode and processes map entries as separate elements - New method:
isPolymorphicWithKeyAsDiscriminator()- Checks if a type uses key-based discrimination - New method:
enrichWithDiscriminator()- Injects discriminator value into item data - New method:
shouldTreatMapAsMultipleRecords()- Determines if map should be split into multiple records - Improved logic: Handles both traditional and key-based polymorphic deserialization
RecordInstanceFactory Changes
- Enhanced:
loadPolymorphicData()- SupportsuseKeymode by extracting discriminator from YAML keys - New logic: Automatically enriches data with discriminator when using
useKey = true
Validator Enhancement
- New validation: Prevents invalid configuration where both
inline = trueanduseKey = trueare set - Error message: Clear feedback when incompatible flags are combined
🧪 Testing
New Test Suite: KeyAsDiscriminatorTest
- Coverage: 485+ lines, 9 comprehensive test cases
- Test scenarios:
- Simple field with key-as-discriminator
- List of polymorphic items
- Map with polymorphic values
- Complex nested configurations
- Mixed traditional and key-based modes
- All three polymorphic implementations (Food, Potion, Trim)
Enhanced Validation Tests
- New test for
useKey + inlineconflict detection - Ensures proper error handling for invalid configurations
📋 Migration Guide
✅ Non-Breaking Change - Existing configurations work unchanged.
To Adopt Key-Based Discriminators:
Before:
@ Polymorphic(key = "type")
public interface ItemMetadata extends Loadable {}
items:
- type: food
nutrition: 8
- type: potion
color: "#FF0000"
After:
@ Polymorphic(key = "type", useKey = true)
public interface ItemMetadata extends Loadable {}
items:
food:
nutrition: 8
potion:
color: "#FF0000"
📊 Comparison Table
| Mode | Discriminator Location | Syntax | Best For | Flags Required |
|---|---|---|---|---|
| Traditional | Inside object as field | type: value | Explicit type marking | @ Polymorphic(key="type") |
| Inline Discriminator | Parent level as field | Parent has type field | Flat structures | @ Polymorphic(inline=true) |
| Key-as-Discriminator ✨ | YAML key itself | keyName: {...} | Clean, intuitive configs | @ Polymorphic(useKey=true) |
⚠️ Important Constraints
Cannot combine: @ Polymorphic(useKey = true, inline = true)
- These modes are mutually exclusive
- Validator will throw
ValidationExceptionif both are enabled - Choose one approach based on your use case
🎯 Best Practices
✅ Use Key-as-Discriminator When:
- Configuration structure naturally groups by type
- Working with named collections (maps)
- Readability is prioritized over explicitness
- Type names are short and descriptive
❌ Avoid When:
- Need inline discriminators at parent level
- Type values contain special characters or spaces
- Lists require explicit ordering with type markers
- Configuration must be extremely explicit
📝 Version
1.3.1 → 1.4.0
Changes:
- ✨ New
@ Polymorphic(useKey)parameter - 🔧 Enhanced
ValueConverterwith key-based logic - 🛡️ New validation for incompatible flag combinations
- 🧪 Comprehensive test suite (+485 lines)
Full Changelog: https://github.com/Traqueur-dev/Structura/compare/1.3.1...1.4.0
1.3.1
🔧 Structura v1.3.1 – Fully Inline Polymorphic Fields
Complete field flattening for polymorphic interfaces.
✨ New Feature
🎯 Fully Inline Polymorphic Fields
Combine @Options(inline = true) with @Polymorphic(inline = true) to flatten all fields (discriminator + implementation fields) to parent level.
@Polymorphic(key = "type", inline = true)
public interface DatabaseConfig extends Loadable {}
public record AppConfig(
String appName,
@Options(inline = true) DatabaseConfig database // FULLY inline
) implements Loadable {}YAML - Before v1.3.1:
app-name: MyApp
type: mysql # Discriminator at parent (v1.3.0)
database: # Fields still nested
host: localhost
port: 3306YAML - After v1.3.1:
app-name: MyApp
type: mysql # Everything at parent level
host: localhost
port: 3306🔧 Implementation
RecordInstanceFactory Changes:
- New method:
enrichParentDataWithDiscriminator()- Validates discriminator presence and prepares data for polymorphic resolution - Enhanced:
isInlineField()- Now detects polymorphic interfaces with@Polymorphic(inline = true) - Enhanced:
resolveComponentValue()- Handles both concrete records and polymorphic interfaces separately
🎮 Use Cases
Simple Configuration:
app-name: MyApp
type: mysql
host: db.example.com
port: 3307Custom Discriminator Key:
app-name: MyApp
provider: s3
bucket: my-data
region: us-west-2Mixed Inline/Nested:
app-name: MyApp
type: mysql # database (fully inline)
host: db.local
cache: # cache (traditional nested)
type: redis
host: cache.local🧪 Testing
- New test suite:
FullyInlinePolymorphicTest(+205 lines, 8 tests) - Tests cover: full flattening, multiple types, custom keys, defaults, mixed configs, error scenarios
📋 Migration Guide
No Breaking Changes - This is an opt-in enhancement.
To adopt:
// Add @Options(inline = true) to your polymorphic field
public record Config(
@Options(inline = true) DatabaseConfig database // Add this annotation
) implements Loadable {}📊 Comparison
| Mode | Discriminator | Fields | Flags Required |
|---|---|---|---|
| Traditional | Inside field | Inside field | None |
| Inline Discriminator | Parent level | Inside field | @Polymorphic(inline=true) |
| Fully Inline ✨ | Parent level | Parent level | @Polymorphic(inline=true) + @Options(inline=true) |
🎯 Best Practices
✅ Use when:
- Simple polymorphic configurations
- Maximum YAML readability needed
- No field name conflicts between inline fields
❌ Avoid when:
- Multiple inline polymorphic fields share field names
- Complex implementations with many fields
- Need clear visual separation
📝 Version
1.3.0 → 1.3.1
Full Changelog: 1.3.0...1.3.1
1.3.0
🚀 Structura v1.3.0 – Inline Fields & Generic Type Readers
Advanced field flattening and generic type conversion for cleaner configurations and better external library integration.
✨ New Features
📦 Inline Fields (Field Flattening)
@Options(inline = true): Flatten nested record fields to parent level, eliminating unnecessary nesting- Multiple inline fields: Support for multiple flattened records in the same configuration
- Mixed configurations: Combine inline and nested fields freely
- Nested inline support: Chain multiple inline levels for maximum flexibility
- Default value integration: Works seamlessly with
@Default*annotations
// Before: Nested structure
public record AppConfig(
String appName,
ServerInfo server // Nested under "server" key
) implements Loadable {}
// YAML with nesting:
// app-name: MyApp
// server:
// host: localhost
// port: 8080
// After: Flattened structure
public record AppConfig(
String appName,
@Options(inline = true) ServerInfo server // Fields at root level
) implements Loadable {}
// YAML without nesting:
// app-name: MyApp
// host: localhost
// port: 8080🎯 Generic Type Readers with TypeToken
TypeToken<T>class: Capture generic type information at compile time (inspired by Gson)- Generic type support: Register custom readers for
List<T>,Optional<T>,Map<K,V>, etc. - Type-specific readers: Different readers for
Box<String>vsBox<Integer> - Priority system: TypeToken readers take precedence over Class readers with automatic fallback
- Thread-safe: Built on the existing
CustomReaderRegistryinfrastructure
// Register reader for List<Component> specifically
CustomReaderRegistry.getInstance().register(
new TypeToken<List<Component>>() {}, // Note the {} - creates anonymous class
str -> parseComponentList(str)
);
// Register different reader for List<String>
CustomReaderRegistry.getInstance().register(
new TypeToken<List<String>>() {},
str -> Arrays.asList(str.split(","))
);
// Use in configuration
public record ServerConfig(
List<Component> messages, // Uses List<Component> reader
List<String> tags // Uses List<String> reader
) implements Loadable {}🔄 Polymorphic Inline Discriminator
@Polymorphic(inline = true): Place discriminator key at parent level instead of inside field value- Custom key names: Works with
@Polymorphic(key = "provider", inline = true) - Improved readability: Cleaner YAML structure for plugin systems and dynamic configurations
- Backward compatible: Default
inline = falsepreserves existing behavior
// Before: Discriminator inside field
@Polymorphic(key = "type")
public interface DatabaseConfig extends Loadable {}
// YAML:
// database:
// type: mysql
// host: localhost
// After: Discriminator at parent level
@Polymorphic(key = "type", inline = true)
public interface DatabaseConfig extends Loadable {}
// YAML:
// type: mysql
// database:
// host: localhost🔧 API Enhancements
CustomReaderRegistry
CustomReaderRegistry registry = CustomReaderRegistry.getInstance();
// NEW: Register with TypeToken for generic types
<T> void register(TypeToken<T> typeToken, Reader<T> reader)
// NEW: Check/unregister with TypeToken
boolean hasReader(TypeToken<?> typeToken)
boolean unregister(TypeToken<?> typeToken)
// ENHANCED: Convert with generic type information
<T> Optional<T> convert(Object value, Type genericType, Class<T> targetClass)
// Existing methods still work
<T> void register(Class<T> targetClass, Reader<T> reader)
boolean hasReader(Class<?> targetClass)
boolean unregister(Class<?> targetClass)
<T> Optional<T> convert(Object value, Class<T> targetClass)
void clear()
int size()TypeToken API
// Create TypeToken for generic types (MUST use {} to create anonymous class)
TypeToken<List<String>> token = new TypeToken<List<String>>() {};
// Factory methods
TypeToken<String> token = TypeToken.of(String.class);
TypeToken<?> token = TypeToken.of(type); // from reflection
// Get type information
Type getType() // Full generic type
Class<? super T> getRawType() // Erased class typeEnhanced Annotations
@Options - New inline parameter:
@Options(
name = "custom-field-name", // Override field name
optional = true, // Mark as optional
isKey = true, // Use for key-based mapping
inline = false // NEW: Flatten record fields (default: false)
)@Polymorphic - New inline parameter:
@Polymorphic(
key = "type", // Discriminator field name (default: "type")
inline = false // NEW: Place discriminator at parent level (default: false)
)📦 New Classes
fr.traqueur.structura.types.TypeToken<T>(150 lines)- Generic type information capture
- Factory methods for Class and Type
- Proper equals/hashCode implementation
- Validation for anonymous class instantiation
🎮 Use Cases
1. Cleaner Configuration Structure
// Instead of deeply nested YAML
public record AppConfig(
@Options(inline = true) ServerInfo server,
@Options(inline = true) AuthInfo auth
) implements Loadable {}
// YAML becomes flat and readable
// host: api.example.com
// port: 8080
// username: admin
// password: secret2. Adventure API Integration with Lists
// Register once at startup
CustomReaderRegistry.getInstance().register(
new TypeToken<List<Component>>() {},
str -> lines.stream()
.map(line -> MiniMessage.miniMessage().deserialize(line))
.toList()
);
// Use in config
public record Messages(
List<Component> welcomeMessages,
List<Component> helpText
) implements Loadable {}3. Comma-Separated Lists
CustomReaderRegistry.getInstance().register(
new TypeToken<List<String>>() {},
str -> Arrays.stream(str.split(","))
.map(String::trim)
.toList()
);
// YAML: tags: "java, yaml, configuration"
// Becomes: ["java", "yaml", "configuration"]4. Optional Values
CustomReaderRegistry.getInstance().register(
new TypeToken<Optional<String>>() {},
str -> str.isEmpty() ? Optional.empty() : Optional.of(str)
);5. Plugin Systems with Inline Discriminators
@Polymorphic(key = "provider", inline = true)
public interface StorageProvider extends Loadable {}
// YAML:
// provider: s3 # Clear and concise
// storage:
// bucket: my-bucket
// region: us-east-1🧪 Testing
New Comprehensive Test Suites
-
InlineFieldsTest(463 lines)- Basic inline field behavior
- Multiple inline fields handling
- Mixed inline and nested configurations
- Nested inline configurations
- Edge cases and validation
- Backward compatibility verification
-
InlinePolymorphicTest(501 lines)- Basic inline polymorphic behavior
- Inline vs non-inline comparison
- Custom discriminator key names
- Error handling scenarios
- Collections with polymorphic inline types
- Backward compatibility
-
GenericTypeReaderIntegrationTest(290 lines)- Generic type reader registration
- Type-specific conversion
- Backward compatibility with Class-based API
- Complex generic types (
Optional<T>,List<T>,Map<K,V>) - Registry management
- Priority and fallback behavior
-
TypeTokenTest(249 lines)- TypeToken creation for various types
- Factory method validation
- Equality and hashCode contracts
- Edge cases and error scenarios
- toString() representation
Total: 1,503+ new test lines with 100+ test cases
📋 Migration Guide
No Breaking Changes
All new features are opt-in with defaults preserving existing behavior:
@Options(inline = false)by default@Polymorphic(inline = false)by default- Existing Class-based CustomReader API unchanged
- All v1.2.0 code works identically in v1.3.0
Adopting New Features
1. Enable Inline Fields:
// Change from:
public record Config(ServerInfo server) implements Loadable {}
// To:
public record Config(
@Options(inline = true) ServerInfo server
) implements Loadable {}
// Update YAML from nested to flat structure2. Use Generic Type Readers:
// At application startup, register once:
CustomReaderRegistry.getInstance().register(
new TypeToken<List<Component>>() {},
str -> parseComponents(str)
);
// Then use List<Component> in any config record3. Enable Inline Discriminators:
// Add inline = true to @Polymorphic:
@Polymorphic(key = "type", inline = true)
public interface MyInterface extends Loadable {}
// Move discriminator key from inside field to parent level in YAML⚙️ Technical Details
Inline Fields Implementation
- Validation: Only works with records implementing
Loadable - Processing: Fields are extracted from parent-level data during record construction
- Default values: Fully compatible with
@Default*annotations - Multiple inline: All inline fields share the parent data map
- Location:
RecordInstanceFactory.isInlineField(),RecordInstanceFactory.resolveComponentValue()
TypeToken Implementation
- Type capture: Uses anonymous class trick to preserve generic type information at runtime
- Storage:
ConcurrentHashMap<TypeToken<?>, Reader<?>>with unified type handling - Lookup strategy:...
1.2.0
🔧 Structura v1.2.0 – Custom Reader System
Extensible type conversion system for external libraries and custom types.
✨ New Features
- Custom Reader System: Register custom converters for types not natively supported by Structura
Reader<T>interface: Simple functional interface for String → T conversionCustomReaderRegistry: Thread-safe singleton registry for managing custom readers- Automatic conversion: Registered readers are automatically used during YAML parsing
- Priority conversion: Custom readers are checked before standard type conversion
🎯 Key Components
// Register a custom reader for Adventure API Component
CustomReaderRegistry.getInstance().register(
Component.class,
str -> MiniMessage.miniMessage().deserialize(str)
);
// Use in configuration records
public record MessageConfig(
Component welcomeMessage, // Automatically converted
Component errorMessage
) implements Loadable {}
// Parse YAML with automatic conversion
String yaml = """
welcome-message: "<green>Welcome!</green>"
error-message: "<red>Error!</red>"
""";
MessageConfig config = Structura.parse(yaml, MessageConfig.class);🔧 API
CustomReaderRegistry methods:
register(Class<T> targetClass, Reader<T> reader)- Register a custom readerunregister(Class<?> targetClass)- Remove a registered readerhasReader(Class<?> targetClass)- Check if a reader existsconvert(Object value, Class<T> targetClass)- Convert a value using registered readerclear()- Remove all readers (useful for testing)size()- Get count of registered readers
📦 New Classes
fr.traqueur.structura.readers.Reader<T>- Functional interface for custom conversionfr.traqueur.structura.registries.CustomReaderRegistry- Registry for managing readers
🔄 Integration
- ValueConverter enhancement: Custom readers are checked first in the conversion pipeline
- Type safety: Uses
Class.cast()for runtime type verification - String-only: Readers only work with String YAML values (use records/polymorphic for complex objects)
- Thread-safe: ConcurrentHashMap-based storage for concurrent access
🎮 Use Cases
- Adventure API Components: Convert MiniMessage strings to Component objects
- Custom Color types: Parse color strings into custom Color classes
- External libraries: Integrate any library requiring custom String parsing
- Domain-specific types: Convert formatted strings to business objects
🧪 Testing
- Complete test suite: 30+ unit tests covering all registry operations
- Integration tests: End-to-end scenarios with mock Adventure API
- Thread safety tests: Concurrent registration validation
- Error handling tests: Comprehensive exception scenario coverage
📋 Migration Guide
No breaking changes - this is a purely additive feature. Existing code continues to work unchanged.
New users can immediately:
- Register readers at application startup
- Use custom types in configuration records
- Parse YAML with automatic conversion
⚙️ Technical Details
- Conversion priority: Custom readers → Polymorphic types → Standard types
- Exception handling: Reader failures wrapped in
StructuraException - Type checking: Runtime verification via
Class.cast() - Storage:
ConcurrentHashMap<Class<?>, Reader<?>>for thread-safe access
🔗 Related Features
- Works alongside
PolymorphicRegistryfor complex object hierarchies - Complements default value annotations for optional fields
- Integrates with validation system for converted values
Full Changelog: 1.1.1...1.2.0
1.1.1
🔧 Structura v1.1.1 – Polymorphic System Restoration
Critical patch restoring polymorphic configuration features lost in merge conflict.
🚨 Critical Fix
- Restored polymorphic configuration system: Complete re-implementation of v1.1.0 features lost due to merge conflict resolution
- Full feature parity: All polymorphic functionality from v1.1.0 has been restored and validated
🔄 Restored Features
@Polymorphicannotation: Type resolution based on YAML keysPolymorphicRegistry<T>: Type-safe implementation registries- Automatic type detection: Runtime resolution of concrete implementations
- Registry management: Creation, configuration, and retrieval of polymorphic registries
- Package reorganization:
DefaultValueRegistryproperly relocated toregistriespackage
📋 What Was Restored
// All polymorphic functionality restored
@Polymorphic(key = "type")
public interface DatabaseConfig extends Loadable { /* ... */ }
PolymorphicRegistry.create(DatabaseConfig.class, registry -> {
registry.register("mysql", MySQLConfig.class);
registry.register("postgres", PostgreSQLConfig.class);
});🧪 Quality Assurance
- Complete test suite: All 250+ polymorphic tests restored and passing
- Feature validation: Comprehensive verification of restored functionality
- Regression testing: Ensures no additional features were lost
📦 Impact
- No API changes: Identical functionality to v1.1.0
- Full compatibility: Existing code using v1.1.0 works unchanged
- Immediate upgrade: Safe to upgrade from v1.1.0 without modifications
⚠️ Recommendation
Users on v1.1.0 should upgrade immediately to v1.1.1 to ensure access to all polymorphic features.
Full Changelog: 1.1.0...1.1.1
1.1.0
🚀 Structura v1.1.0 – Polymorphic Configuration Support
Major feature release introducing polymorphic type resolution and improved registry architecture.
✨ New Features
🔄 Polymorphic Configuration System
@Polymorphicannotation: Enable automatic type resolution based on YAML keys- Type-safe polymorphic registries: Register multiple implementations for a single interface
- Automatic implementation detection: Resolve concrete types at runtime based on configuration data
- Custom key mapping: Configure which YAML field determines the implementation type
@Polymorphic(key = "type")
public interface DatabaseConfig extends Loadable {
String getHost();
int getPort();
}
// Register implementations
PolymorphicRegistry.create(DatabaseConfig.class, registry -> {
registry.register("mysql", MySQLConfig.class);
registry.register("postgres", PostgreSQLConfig.class);
});
// YAML automatically resolves to correct implementation
// type: mysql
// host: "localhost"
// port: 3306🏗️ Enhanced Registry Architecture
PolymorphicRegistry<T>: Type-safe registry for managing implementation mappings- Fluent configuration API: Clean, builder-style registry setup
- Case-insensitive resolution: Flexible type name matching
- Automatic class name registration: Register using simple class names
- Registry isolation: Separate registries per interface type
🔧 Internal Improvements
- Reorganized package structure: Moved registries to dedicated
registriespackage DefaultValueRegistryrelocated tofr.traqueur.structura.registries- Enhanced error handling: More descriptive exceptions for registry operations
- Comprehensive test coverage: 250+ new test cases for polymorphic functionality
📚 API Reference
Registry Management
// Create and configure registry
PolymorphicRegistry.create(MyInterface.class, registry -> {
registry.register("impl1", Implementation1.class);
registry.register("impl2", Implementation2.class);
registry.register(Implementation3.class); // Auto-naming
});
// Retrieve registry
PolymorphicRegistry<MyInterface> registry = PolymorphicRegistry.get(MyInterface.class);
// Query implementations
Optional<Class<? extends MyInterface>> impl = registry.get("impl1");
Set<String> availableTypes = registry.availableNames();Polymorphic Annotation
@Polymorphic(key = "provider") // Custom key (default: "type")
public interface PaymentProvider extends Loadable {
void processPayment(double amount);
}🎯 Use Cases
- Plugin systems: Dynamically load different implementations based on configuration
- Service providers: Switch between payment processors, databases, etc.
- Environment-specific configs: Different implementations for dev/staging/prod
- Feature flags: Enable/disable functionality through configuration
- Modular architectures: Clean separation of interface and implementation
🛡️ Error Handling
- Registry validation: Prevents duplicate registrations and null values
- Type safety: Compile-time guarantees for polymorphic resolution
- Descriptive exceptions: Clear error messages for configuration issues
- Graceful fallbacks: Empty optionals for missing implementations
📦 Migration Notes
- Package changes:
DefaultValueRegistrymoved toregistriespackage - No breaking API changes: Existing configuration code remains compatible
- Optional feature: Polymorphic support is opt-in via annotations
🧪 Quality Assurance
- Comprehensive test suite: 250+ tests covering core functionality and edge cases
- Type safety validation: Ensures registry isolation and type correctness
- Error scenario coverage: Validates exception handling and error messages
- Performance testing: Efficient registry lookup and resolution
Full Changelog: 1.0.3...1.1.0
1.0.3
🚀 Structura v1.0.3 – Key Mapping & Architecture Refactoring
Major feature addition with key-based YAML mapping and comprehensive architecture improvements.
✨ New Features
-
🔑 Key-based YAML mapping with
@Options(isKey = true)- Simple key mapping: YAML keys become field values for primitive types
- Complex object flattening: Record fields are extracted to the same level as other fields
- Recursive key mapping: Works at any nesting level in your configuration hierarchy
- Type-safe key conversion: Automatic conversion between YAML keys and Java field types
-
🎯 Enhanced validation system
@Min/@Max: Numeric range validation with custom error messages@Size: Collection and string size constraints@NotEmpty: Ensures collections, maps, and strings are not empty@Pattern: Regex pattern matching for string fields- Nested validation: Automatically validates nested records and enum fields
- Custom error messages: Placeholder support for dynamic error formatting
🏗️ Architecture Improvements
-
⚡ Modular design: Split monolithic
StructuraProcessorinto specialized components:FieldMapper: Handles name conversion and key mapping logicValueConverter: Manages type conversions and collection handlingRecordInstanceFactory: Creates record instances with key mapping supportValidator: Comprehensive validation engine
-
🔄 Improved type conversion:
- Better enum case handling (supports lowercase, uppercase, mixed case)
- Enhanced collection type conversion with proper generics support
- More robust primitive type conversion with detailed error messages
- Support for
char,byte,short,floattypes
-
🧪 Comprehensive test coverage:
- 1,200+ new test cases covering all features
- Integration tests for real-world scenarios
- Performance tests for large configurations
- Edge case validation and error handling tests
📚 Documentation Updates
- 📖 Updated README with key mapping examples and complete API reference
- 🎯 Key mapping documentation: Simple vs. complex key mapping with practical examples
- 🔧 Validation annotations guide: Complete reference with usage patterns
- 🏛️ Architecture documentation: Component responsibilities and interaction patterns
🛠️ API Enhancements
-
🎛️ Custom processor configuration:
var processor = Structura.builder() .withValidation(false) // Disable validation if needed .build(); Structura.with(processor);
-
🗂️ Enhanced file operations: Better error messages and resource handling
-
🔍 Improved debugging: More descriptive exception messages with context paths
📈 Example: Key Mapping
// Simple key mapping
public record DatabaseConnection(
@Options(isKey = true) String name,
String host,
int port
) implements Loadable {}
// YAML: production: { host: "db.prod.com", port: 5432 }
// Result: name="production", host="db.prod.com", port=5432
// Complex object flattening
public record AppConfig(
@Options(isKey = true) ServerInfo server,
String appName
) implements Loadable {}
// YAML: { host: "api.com", port: 8080, app-name: "MyApp" }
// Result: server.host="api.com", server.port=8080, appName="MyApp"🔧 Breaking Changes
- 🏗️ Internal architecture: Complete refactoring of internal components (API remains compatible)
- 📝 Interface updates: All documentation references updated from
SettingstoLoadable
🐛 Bug Fixes
- ✅ Fixed collection type conversion edge cases
- ✅ Improved enum constant initialization reliability
- ✅ Better handling of null and optional values in nested structures
- ✅ Enhanced file path resolution for different operating systems
Full Changelog: 1.0.2...1.0.3
1.0.2
🐛 Structura v1.0.2 – Patch Release
🔧 Fixes
- ✅ Fixed: Incorrect mapping of enum constant field names from snake_case instead of proper kebab-case when loading from YAML.
→ Enum fields now correctly respect kebab-case mapping like the rest of the configuration system.
Full Changelog: 1.0.1...1.0.2