feat(weaver-pojo-presets-ext): Implement POJO weaving for presets

This commit is contained in:
2025-11-02 19:46:58 +01:00
parent 2b0fac4127
commit 36dff476ea
13 changed files with 283 additions and 6 deletions

View File

@@ -19,6 +19,7 @@ dependencies {
implementation("de.siphalor.tweed5:tweed5-serde-extension") implementation("de.siphalor.tweed5:tweed5-serde-extension")
implementation("de.siphalor.tweed5:tweed5-weaver-pojo") implementation("de.siphalor.tweed5:tweed5-weaver-pojo")
implementation("de.siphalor.tweed5:tweed5-weaver-pojo-attributes-extension") implementation("de.siphalor.tweed5:tweed5-weaver-pojo-attributes-extension")
implementation("de.siphalor.tweed5:tweed5-weaver-pojo-presets-extension")
implementation("de.siphalor.tweed5:tweed5-weaver-pojo-serde-extension") implementation("de.siphalor.tweed5:tweed5-weaver-pojo-serde-extension")
implementation("de.siphalor.tweed5:tweed5-weaver-pojo-validation-extension") implementation("de.siphalor.tweed5:tweed5-weaver-pojo-validation-extension")
} }

View File

@@ -9,6 +9,8 @@ import de.siphalor.tweed5.defaultextensions.validation.api.ValidationExtension;
import de.siphalor.tweed5.defaultextensions.validationfallback.api.ValidationFallbackExtension; import de.siphalor.tweed5.defaultextensions.validationfallback.api.ValidationFallbackExtension;
import de.siphalor.tweed5.weaver.pojo.api.annotation.*; import de.siphalor.tweed5.weaver.pojo.api.annotation.*;
import de.siphalor.tweed5.weaver.pojoext.attributes.api.AttributesPojoWeavingProcessor; import de.siphalor.tweed5.weaver.pojoext.attributes.api.AttributesPojoWeavingProcessor;
import de.siphalor.tweed5.weaver.pojoext.presets.api.DefaultPresetWeavingProcessor;
import de.siphalor.tweed5.weaver.pojoext.presets.api.PresetsWeavingProcessor;
import de.siphalor.tweed5.weaver.pojoext.serde.api.auto.AutoReadWritePojoWeavingProcessor; import de.siphalor.tweed5.weaver.pojoext.serde.api.auto.AutoReadWritePojoWeavingProcessor;
import de.siphalor.tweed5.weaver.pojoext.serde.api.auto.DefaultReadWriteMappings; import de.siphalor.tweed5.weaver.pojoext.serde.api.auto.DefaultReadWriteMappings;
import de.siphalor.tweed5.weaver.pojoext.validation.api.ValidatorsPojoWeavingProcessor; import de.siphalor.tweed5.weaver.pojoext.validation.api.ValidatorsPojoWeavingProcessor;
@@ -26,7 +28,9 @@ import java.lang.annotation.Target;
DefaultReadWriteMappings.class, DefaultReadWriteMappings.class,
CompoundWeaving.class CompoundWeaving.class
}) })
@PojoWeaving
@TweedExtension(ReadWriteExtension.class) @TweedExtension(ReadWriteExtension.class)
@TweedExtension(PresetsExtension.class)
@TweedExtension(ValidationExtension.class) @TweedExtension(ValidationExtension.class)
@TweedExtension(ValidationFallbackExtension.class) @TweedExtension(ValidationFallbackExtension.class)
@TweedExtension(AttributesExtension.class) @TweedExtension(AttributesExtension.class)
@@ -34,6 +38,8 @@ import java.lang.annotation.Target;
@PojoWeavingExtension(AutoReadWritePojoWeavingProcessor.class) @PojoWeavingExtension(AutoReadWritePojoWeavingProcessor.class)
@PojoWeavingExtension(ValidatorsPojoWeavingProcessor.class) @PojoWeavingExtension(ValidatorsPojoWeavingProcessor.class)
@PojoWeavingExtension(AttributesPojoWeavingProcessor.class) @PojoWeavingExtension(AttributesPojoWeavingProcessor.class)
@PojoWeavingExtension(PresetsWeavingProcessor.class)
@PojoWeavingExtension(DefaultPresetWeavingProcessor.class)
@DefaultWeavingExtensions @DefaultWeavingExtensions
@DefaultReadWriteMappings @DefaultReadWriteMappings
@CompoundWeaving @CompoundWeaving

View File

@@ -8,6 +8,7 @@ import de.siphalor.tweed5.dataapi.api.TweedDataWriter;
import de.siphalor.tweed5.dataapi.api.TweedSerde; import de.siphalor.tweed5.dataapi.api.TweedSerde;
import de.siphalor.tweed5.defaultextensions.patch.api.PatchExtension; import de.siphalor.tweed5.defaultextensions.patch.api.PatchExtension;
import de.siphalor.tweed5.defaultextensions.patch.api.PatchInfo; import de.siphalor.tweed5.defaultextensions.patch.api.PatchInfo;
import de.siphalor.tweed5.defaultextensions.presets.api.PresetsExtension;
import de.siphalor.tweed5.patchwork.api.Patchwork; import de.siphalor.tweed5.patchwork.api.Patchwork;
import lombok.Getter; import lombok.Getter;
import lombok.extern.apachecommons.CommonsLog; import lombok.extern.apachecommons.CommonsLog;
@@ -29,6 +30,7 @@ public class FabricConfigContainerHelper<T extends @Nullable Object> {
private final ConfigContainer<T> configContainer; private final ConfigContainer<T> configContainer;
private final ReadWriteExtension readWriteExtension; private final ReadWriteExtension readWriteExtension;
private final @Nullable PatchExtension patchExtension; private final @Nullable PatchExtension patchExtension;
private final @Nullable PresetsExtension presetsExtension;
private final TweedSerde serde; private final TweedSerde serde;
@Getter @Getter
private final String modId; private final String modId;
@@ -54,10 +56,16 @@ public class FabricConfigContainerHelper<T extends @Nullable Object> {
this.readWriteExtension = configContainer.extension(ReadWriteExtension.class) this.readWriteExtension = configContainer.extension(ReadWriteExtension.class)
.orElseThrow(() -> new IllegalStateException("ReadWriteExtension not declared in config container")); .orElseThrow(() -> new IllegalStateException("ReadWriteExtension not declared in config container"));
this.patchExtension = configContainer.extension(PatchExtension.class).orElse(null); this.patchExtension = configContainer.extension(PatchExtension.class).orElse(null);
this.presetsExtension = configContainer.extension(PresetsExtension.class).orElse(null);
this.serde = serde; this.serde = serde;
this.modId = modId; this.modId = modId;
} }
public T loadAndUpdateInConfigDirectory() {
T defaultPresetValue = getDefaultPresetValue();
return loadAndUpdateInConfigDirectory(() -> configContainer.rootEntry().deepCopy(defaultPresetValue));
}
public T loadAndUpdateInConfigDirectory(Supplier<T> defaultValueSupplier) { public T loadAndUpdateInConfigDirectory(Supplier<T> defaultValueSupplier) {
T configValue = readConfigInConfigDirectory(defaultValueSupplier); T configValue = readConfigInConfigDirectory(defaultValueSupplier);
writeConfigInConfigDirectory(configValue); writeConfigInConfigDirectory(configValue);
@@ -89,6 +97,11 @@ public class FabricConfigContainerHelper<T extends @Nullable Object> {
} }
} }
public T readConfigInConfigDirectory() {
T defaultPresetValue = getDefaultPresetValue();
return readConfigInConfigDirectory(() -> configContainer.rootEntry().deepCopy(defaultPresetValue));
}
public T readConfigInConfigDirectory(Supplier<T> defaultValueSupplier) { public T readConfigInConfigDirectory(Supplier<T> defaultValueSupplier) {
File configFile = getConfigFile(); File configFile = getConfigFile();
if (!configFile.exists()) { if (!configFile.exists()) {
@@ -158,4 +171,13 @@ public class FabricConfigContainerHelper<T extends @Nullable Object> {
} }
return tempConfigDirectory; return tempConfigDirectory;
} }
private T getDefaultPresetValue() {
if (presetsExtension == null) {
throw new IllegalStateException(
"No presets extension registered, either register such extension or provide a default value manually"
);
}
return presetsExtension.presetValue(configContainer.rootEntry(), PresetsExtension.DEFAULT_PRESET_NAME);
}
} }

View File

@@ -32,6 +32,7 @@ includeNormalModule("type-utils")
includeNormalModule("utils") includeNormalModule("utils")
includeNormalModule("weaver-pojo") includeNormalModule("weaver-pojo")
includeNormalModule("weaver-pojo-attributes-extension") includeNormalModule("weaver-pojo-attributes-extension")
includeNormalModule("weaver-pojo-presets-extension")
includeNormalModule("weaver-pojo-serde-extension") includeNormalModule("weaver-pojo-serde-extension")
includeNormalModule("weaver-pojo-validation-extension") includeNormalModule("weaver-pojo-validation-extension")

View File

@@ -0,0 +1,9 @@
plugins {
id("de.siphalor.tweed5.base-module")
}
dependencies {
api(project(":tweed5-construct"))
api(project(":tweed5-default-extensions"))
api(project(":tweed5-weaver-pojo"))
}

View File

@@ -0,0 +1,2 @@
module.name = Presets Extension for Tweed 5 Weaver POJO
module.description = Allows declaring presets on POJOs using annotations.

View File

@@ -0,0 +1,48 @@
package de.siphalor.tweed5.weaver.pojoext.presets.api;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.defaultextensions.presets.api.PresetsExtension;
import de.siphalor.tweed5.weaver.pojo.api.weaving.TweedPojoWeavingExtension;
import lombok.RequiredArgsConstructor;
import lombok.extern.apachecommons.CommonsLog;
@CommonsLog
@RequiredArgsConstructor
public class DefaultPresetWeavingProcessor<T> implements TweedPojoWeavingExtension {
private final ConfigContainer<T> configContainer;
@Override
public void setup(SetupContext context) {
}
@Override
public void afterWeave() {
PresetsExtension presetsExtension = configContainer.extension(PresetsExtension.class)
.orElseThrow(() -> new IllegalStateException(
"Can only use " + DefaultPresetWeavingProcessor.class.getSimpleName()
+ " if " + PresetsExtension.class.getSimpleName() + " is registered"
));
if (presetsExtension.presetValue(configContainer.rootEntry(), PresetsExtension.DEFAULT_PRESET_NAME) != null) {
log.debug("Default preset already registered, skipping auto instantiation");
return;
}
T defaultValue = instantiateEntry(configContainer.rootEntry());
presetsExtension.presetValue(configContainer.rootEntry(), PresetsExtension.DEFAULT_PRESET_NAME, defaultValue);
}
private T instantiateEntry(ConfigEntry<T> entry) {
if (entry instanceof CompoundConfigEntry) {
return ((CompoundConfigEntry<T>) entry).instantiateCompoundValue();
} else {
throw new IllegalArgumentException(
"Can only determine default preset from instantiation for POJOs. "
+ "Only apply " + getClass().getSimpleName() + " to POJOs."
);
}
}
}

View File

@@ -0,0 +1,12 @@
package de.siphalor.tweed5.weaver.pojoext.presets.api;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
public @interface Preset {
String value();
}

View File

@@ -0,0 +1,75 @@
package de.siphalor.tweed5.weaver.pojoext.presets.api;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.defaultextensions.presets.api.PresetsExtension;
import de.siphalor.tweed5.typeutils.api.type.ActualType;
import de.siphalor.tweed5.weaver.pojo.api.weaving.TweedPojoWeavingExtension;
import de.siphalor.tweed5.weaver.pojo.api.weaving.WeavingContext;
import lombok.extern.apachecommons.CommonsLog;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;
@CommonsLog
public class PresetsWeavingProcessor implements TweedPojoWeavingExtension {
private static final int REQUIRED_FIELD_MODIFIERS = Modifier.STATIC | Modifier.FINAL;
@Override
public void setup(SetupContext context) {
}
@Override
public <T> void afterWeaveEntry(ActualType<T> valueType, ConfigEntry<T> configEntry, WeavingContext context) {
Map<String, T> presets = new HashMap<>();
for (Field field : valueType.declaredType().getFields()) {
Preset presetAnnotation = field.getAnnotation(Preset.class);
if (presetAnnotation == null) {
continue;
}
if ((field.getModifiers() & REQUIRED_FIELD_MODIFIERS) != REQUIRED_FIELD_MODIFIERS) {
log.warn(
"@Preset field " + field.getName() + " in class " + field.getDeclaringClass().getName()
+ " is not static and final, skipping preset"
);
continue;
}
if (presets.containsKey(presetAnnotation.value())) {
log.warn(
"Duplicate preset name " + presetAnnotation.value() + " in class "
+ field.getDeclaringClass().getName() + ", skipping preset"
);
presets.remove(presetAnnotation.value());
continue;
}
if (!configEntry.valueClass().isAssignableFrom(field.getType())) {
log.warn(
"@Preset field " + field.getName() + " in class " + field.getDeclaringClass().getName()
+ " has incompatible type, skipping preset"
);
continue;
}
try {
//noinspection unchecked
presets.put(presetAnnotation.value(), (T) field.get(null));
} catch (IllegalAccessException e) {
log.warn(
"Failed to access preset field " + field.getName() + " in class "
+ field.getDeclaringClass().getName(),
e
);
}
}
if (!presets.isEmpty()) {
PresetsExtension presetsExtension = configEntry.container().extension(PresetsExtension.class)
.orElseThrow(() -> new IllegalStateException("PresetsExtension not declared in config container"));
presets.forEach((name, value) -> presetsExtension.presetValue(configEntry, name, value));
}
}
}

View File

@@ -0,0 +1,4 @@
@NullMarked
package de.siphalor.tweed5.weaver.pojoext.presets.api;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,82 @@
package de.siphalor.tweed5.weaver.pojoext.presets.api;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.CompoundConfigEntry;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.defaultextensions.presets.api.PresetsExtension;
import de.siphalor.tweed5.weaver.pojo.api.annotation.*;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.TweedPojoWeaverBootstrapper;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.NoArgsConstructor;
import lombok.ToString;
import org.junit.jupiter.api.Test;
import static de.siphalor.tweed5.defaultextensions.presets.api.PresetsExtension.presetValue;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.InstanceOfAssertFactories.type;
class PresetsWeavingProcessorTest {
@Test
void test() {
ConfigContainer<Config> configContainer = TweedPojoWeaverBootstrapper.create(Config.class).weave();
configContainer.initialize();
PresetsExtension presetsExtension = configContainer.extension(PresetsExtension.class).orElseThrow();
Config defaultPreset =
presetsExtension.presetValue(configContainer.rootEntry(), PresetsExtension.DEFAULT_PRESET_NAME);
Config presetA = presetsExtension.presetValue(configContainer.rootEntry(), "a");
Config presetB = presetsExtension.presetValue(configContainer.rootEntry(), "b");
assertThat(defaultPreset).isEqualTo(new Config());
assertThat(presetA).isEqualTo(Config.PRESET_A);
assertThat(presetB).isEqualTo(Config.PRESET_B);
assertThat(configContainer.rootEntry()).asInstanceOf(type(CompoundConfigEntry.class)).satisfies(
compound -> assertThat(compound.subEntries().get("subConfig"))
.asInstanceOf(type(ConfigEntry.class))
.satisfies(
entry -> assertThat(entry.call(presetValue(PresetsExtension.DEFAULT_PRESET_NAME)))
.isEqualTo(new SubConfig("DEFAULT")),
entry -> assertThat(entry.call(presetValue("a"))).isEqualTo(new SubConfig("AAA")),
entry -> assertThat(entry.call(presetValue("b"))).isEqualTo(new SubConfig("BBB")),
entry -> assertThat(entry.call(presetValue("special"))).isEqualTo(SubConfig.SPECIAL_PRESET)
)
);
}
@PojoWeaving
@TweedExtension(PresetsExtension.class)
@PojoWeavingExtension(DefaultPresetWeavingProcessor.class)
@PojoWeavingExtension(PresetsWeavingProcessor.class)
@DefaultWeavingExtensions
@CompoundWeaving
// lombok
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString
public static class Config {
@Preset("a")
public static final Config PRESET_A = new Config("a", 1, true, new SubConfig("AAA"));
@Preset("b")
public static final Config PRESET_B = new Config("b", 2, false, new SubConfig("BBB"));
public String string = "default";
public int integer = 1234;
public boolean bool;
public SubConfig subConfig = new SubConfig("DEFAULT");
}
@CompoundWeaving
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode
@ToString
public static class SubConfig {
@Preset("special")
public static final SubConfig SPECIAL_PRESET = new SubConfig("SPECIAL");
public String value;
}
}

View File

@@ -27,6 +27,8 @@ public interface TweedPojoWeavingExtension extends TweedPojoWeavingFunction {
default <T> void afterWeaveEntry(ActualType<T> valueType, ConfigEntry<T> configEntry, WeavingContext context) {} default <T> void afterWeaveEntry(ActualType<T> valueType, ConfigEntry<T> configEntry, WeavingContext context) {}
default <T> void afterWeave() {}
interface SetupContext { interface SetupContext {
<E> PatchworkPartAccess<E> registerWeavingContextExtensionData(Class<E> dataClass); <E> PatchworkPartAccess<E> registerWeavingContextExtensionData(Class<E> dataClass);
} }

View File

@@ -123,6 +123,8 @@ public class TweedPojoWeaverBootstrapper<T> {
configContainer.attachTree(rootEntry); configContainer.attachTree(rootEntry);
runAfterWeaveHooks();
return configContainer; return configContainer;
} }
@@ -145,7 +147,7 @@ public class TweedPojoWeaverBootstrapper<T> {
) { ) {
extensionsData = extensionsData.copy(); extensionsData = extensionsData.copy();
runBeforeWeaveHooks(valueType, extensionsData, protoContext); runBeforeWeaveEntryHooks(valueType, extensionsData, protoContext);
WeavingContext context = WeavingContext.builder() WeavingContext context = WeavingContext.builder()
.parent(protoContext.parent()) .parent(protoContext.parent())
@@ -161,7 +163,7 @@ public class TweedPojoWeaverBootstrapper<T> {
try { try {
ConfigEntry<U> configEntry = weavingExtension.weaveEntry(valueType, context); ConfigEntry<U> configEntry = weavingExtension.weaveEntry(valueType, context);
if (configEntry != null) { if (configEntry != null) {
runAfterWeaveHooks(valueType, configEntry, context); runAfterWeaveEntryHooks(valueType, configEntry, context);
return configEntry; return configEntry;
} }
} catch (Exception e) { } catch (Exception e) {
@@ -175,7 +177,12 @@ public class TweedPojoWeaverBootstrapper<T> {
throw new PojoWeavingException( throw new PojoWeavingException(
"Failed to weave entry for " + valueType + " at " + Arrays.toString(context.path()) "Failed to weave entry for " + valueType + " at " + Arrays.toString(context.path())
+ ": No matching weavers found" + ": No matching weavers found.\n"
+ "Registered weaving extensions: "
+ weavingExtensions.stream()
.map(TweedPojoWeavingExtension::getClass)
.map(Class::getName)
.collect(Collectors.joining(", "))
); );
} }
@@ -207,7 +214,7 @@ public class TweedPojoWeaverBootstrapper<T> {
try { try {
ConfigEntry<U> configEntry = weavingExtension.weaveEntry(valueType, context); ConfigEntry<U> configEntry = weavingExtension.weaveEntry(valueType, context);
if (configEntry != null) { if (configEntry != null) {
runAfterWeaveHooks(valueType, configEntry, context); runAfterWeaveEntryHooks(valueType, configEntry, context);
return configEntry; return configEntry;
} }
} catch (Exception e) { } catch (Exception e) {
@@ -225,7 +232,7 @@ public class TweedPojoWeaverBootstrapper<T> {
); );
} }
private <U> void runBeforeWeaveHooks( private <U> void runBeforeWeaveEntryHooks(
ActualType<U> dataClass, ActualType<U> dataClass,
Patchwork extensionsData, Patchwork extensionsData,
ProtoWeavingContext protoContext ProtoWeavingContext protoContext
@@ -243,7 +250,7 @@ public class TweedPojoWeaverBootstrapper<T> {
} }
} }
private <U> void runAfterWeaveHooks(ActualType<U> dataClass, ConfigEntry<U> configEntry, WeavingContext context) { private <U> void runAfterWeaveEntryHooks(ActualType<U> dataClass, ConfigEntry<U> configEntry, WeavingContext context) {
for (TweedPojoWeavingExtension weavingExtension : weavingExtensions) { for (TweedPojoWeavingExtension weavingExtension : weavingExtensions) {
try { try {
weavingExtension.afterWeaveEntry(dataClass, configEntry, context); weavingExtension.afterWeaveEntry(dataClass, configEntry, context);
@@ -256,4 +263,10 @@ public class TweedPojoWeaverBootstrapper<T> {
} }
} }
} }
private void runAfterWeaveHooks() {
for (TweedPojoWeavingExtension weavingExtension : weavingExtensions) {
weavingExtension.afterWeave();
}
}
} }