[build] Restructure to composite build

This commit is contained in:
2025-10-26 02:03:53 +02:00
parent 0e3990aed9
commit 1fbc97866c
348 changed files with 126 additions and 64 deletions

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 = Validation Extension for Tweed 5 Weaver POJO
module.description = Allows declaring validation constraints on POJOs using annotations.

View File

@@ -0,0 +1,13 @@
package de.siphalor.tweed5.weaver.pojoext.validation.api;
import de.siphalor.tweed5.weaver.pojoext.validation.api.validators.WeavableConfigEntryValidator;
import java.lang.annotation.*;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.TYPE_USE, ElementType.ANNOTATION_TYPE})
@Repeatable(Validators.class)
public @interface Validator {
Class<? extends WeavableConfigEntryValidator> value();
String config() default "";
}

View File

@@ -0,0 +1,12 @@
package de.siphalor.tweed5.weaver.pojoext.validation.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.TYPE, ElementType.TYPE_USE, ElementType.ANNOTATION_TYPE})
public @interface Validators {
Validator[] value();
}

View File

@@ -0,0 +1,50 @@
package de.siphalor.tweed5.weaver.pojoext.validation.api;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.defaultextensions.validation.api.ConfigEntryValidator;
import de.siphalor.tweed5.defaultextensions.validation.api.ValidationExtension;
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 de.siphalor.tweed5.weaver.pojoext.validation.api.validators.WeavableConfigEntryValidator;
import org.jetbrains.annotations.ApiStatus;
import java.util.Arrays;
public class ValidatorsPojoWeavingProcessor implements TweedPojoWeavingExtension {
private final ValidationExtension validationExtension;
@ApiStatus.Internal
public ValidatorsPojoWeavingProcessor(ConfigContainer<?> configContainer) {
validationExtension = configContainer.extension(ValidationExtension.class)
.orElseThrow(() -> new IllegalStateException(
"You must register a " + ValidationExtension.class.getSimpleName()
+ " to use " + getClass().getSimpleName()
));
}
@Override
public void setup(SetupContext context) {
}
@Override
public <T> void afterWeaveEntry(ActualType<T> valueType, ConfigEntry<T> configEntry, WeavingContext context) {
Validator[] validatorAnnotations = context.annotations().getAnnotationsByType(Validator.class);
ConfigEntryValidator[] validators = Arrays.stream(validatorAnnotations)
.map(validatorAnnotation -> createValidatorFromAnnotation(configEntry, validatorAnnotation))
.toArray(ConfigEntryValidator[]::new);
validationExtension.addValidators(configEntry, validators);
}
private ConfigEntryValidator createValidatorFromAnnotation(
ConfigEntry<?> configEntry,
Validator validatorAnnotation
) {
return WeavableConfigEntryValidator.FACTORY.construct(validatorAnnotation.value())
.typedArg(ConfigEntry.class, configEntry)
.namedArg("config", validatorAnnotation.config())
.finish();
}
}

View File

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

View File

@@ -0,0 +1,13 @@
package de.siphalor.tweed5.weaver.pojoext.validation.api.validators;
import de.siphalor.tweed5.construct.api.TweedConstructFactory;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.defaultextensions.validation.api.ConfigEntryValidator;
public interface WeavableConfigEntryValidator extends ConfigEntryValidator {
TweedConstructFactory<WeavableConfigEntryValidator> FACTORY =
TweedConstructFactory.builder(WeavableConfigEntryValidator.class)
.typedArg(ConfigEntry.class)
.namedArg("config", String.class)
.build();
}

View File

@@ -0,0 +1,115 @@
package de.siphalor.tweed5.weaver.pojoext.validation.api.validators;
import de.siphalor.tweed5.construct.api.ConstructParameter;
import de.siphalor.tweed5.construct.api.TweedConstruct;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationResult;
import de.siphalor.tweed5.defaultextensions.validation.api.validators.NumberRangeValidator;
import lombok.AccessLevel;
import lombok.RequiredArgsConstructor;
import lombok.Value;
import lombok.val;
import org.jetbrains.annotations.ApiStatus;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Value
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public class WeavableNumberRangeValidator implements WeavableConfigEntryValidator {
private static final String NUMBER_PATTERN = "[+-]?\\d+(?:\\.\\d+)?(?:[eE][+-]?\\d+)?";
private static final Pattern CONFIG_PATTERN =
Pattern.compile("^(" + NUMBER_PATTERN + "=?)?\\.\\.(=?" + NUMBER_PATTERN + ")?$");
NumberRangeValidator<Number> validator;
@ApiStatus.Internal
@TweedConstruct(WeavableConfigEntryValidator.class)
public static WeavableNumberRangeValidator construct(
ConfigEntry<?> configEntry,
@ConstructParameter(name = "config") String config
) {
if (config.isEmpty()) {
throw new IllegalArgumentException("Config is required for number range validator");
}
Matcher matcher = CONFIG_PATTERN.matcher(config);
if (!matcher.matches()) {
throw new IllegalArgumentException(
"Invalid config: " + config + "; expected format: [<min>[=]]..[[=]<max>]"
);
}
//noinspection unchecked
Class<? extends Number> numberClass = boxClass((Class<? extends Number>) configEntry.valueClass());
//noinspection unchecked
val builder = NumberRangeValidator.builder((Class<Number>) numberClass);
String minGroup = matcher.group(1);
if (minGroup != null) {
if (minGroup.endsWith("=")) {
minGroup = minGroup.substring(0, minGroup.length() - 1);
builder.greaterThanOrEqualTo(parseNumber(minGroup, numberClass));
} else {
builder.greaterThan(parseNumber(minGroup, numberClass));
}
}
String maxGroup = matcher.group(2);
if (maxGroup != null) {
if (maxGroup.startsWith("=")) {
maxGroup = maxGroup.substring(1);
builder.lessThanOrEqualTo(parseNumber(maxGroup, numberClass));
} else {
builder.lessThan(parseNumber(maxGroup, numberClass));
}
}
return new WeavableNumberRangeValidator(builder.build());
}
private static Class<? extends Number> boxClass(Class<? extends Number> numberClass) {
if (numberClass == byte.class) {
return Byte.class;
} else if (numberClass == short.class) {
return Short.class;
} else if (numberClass == int.class) {
return Integer.class;
} else if (numberClass == long.class) {
return Long.class;
} else if (numberClass == float.class) {
return Float.class;
} else if (numberClass == double.class) {
return Double.class;
}
return numberClass;
}
private static Number parseNumber(String number, Class<? extends Number> numberClass) {
if (numberClass == Byte.class) {
return Byte.valueOf(number);
} else if (numberClass == Short.class) {
return Short.valueOf(number);
} else if (numberClass == Integer.class) {
return Integer.valueOf(number);
} else if (numberClass == Long.class) {
return Long.valueOf(number);
} else if (numberClass == Float.class) {
return Float.valueOf(number);
} else if (numberClass == Double.class) {
return Double.valueOf(number);
} else {
throw new IllegalArgumentException("Unsupported number class: " + numberClass.getName());
}
}
@Override
public <T> ValidationResult<T> validate(ConfigEntry<T> configEntry, T value) {
return validator.validate(configEntry, value);
}
@Override
public <T> String description(ConfigEntry<T> configEntry) {
return validator.description(configEntry);
}
}

View File

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

View File

@@ -0,0 +1,53 @@
package de.siphalor.tweed5.weaver.pojoext.validation.api;
import de.siphalor.tweed5.core.api.container.ConfigContainer;
import de.siphalor.tweed5.defaultextensions.validation.api.ValidationExtension;
import de.siphalor.tweed5.defaultextensions.validation.api.result.ValidationIssues;
import de.siphalor.tweed5.weaver.pojo.api.annotation.CompoundWeaving;
import de.siphalor.tweed5.weaver.pojo.api.annotation.DefaultWeavingExtensions;
import de.siphalor.tweed5.weaver.pojo.api.annotation.PojoWeaving;
import de.siphalor.tweed5.weaver.pojo.api.annotation.PojoWeavingExtension;
import de.siphalor.tweed5.weaver.pojo.impl.weaving.TweedPojoWeaverBootstrapper;
import de.siphalor.tweed5.weaver.pojoext.validation.api.validators.WeavableNumberRangeValidator;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import static org.assertj.core.api.Assertions.assertThat;
class ValidatorsPojoWeavingProcessorTest {
@ParameterizedTest
@CsvSource({"-1,true", "0,false", "50,false", "99,false", "100,true", "101,true"})
void test(int value, boolean issuesExpected) {
var bootstrapper = TweedPojoWeaverBootstrapper.create(Config.class);
ConfigContainer<Config> configContainer = bootstrapper.weave();
configContainer.initialize();
var validationExtension = configContainer.extension(ValidationExtension.class).orElseThrow();
ValidationIssues issues = validationExtension.validate(configContainer.rootEntry(), new Config(value));
if (issuesExpected) {
assertThat(issues.issuesByPath()).as("Issues should be present").isNotEmpty();
} else {
assertThat(issues.issuesByPath()).as("No issues should be present").isEmpty();
}
}
@PojoWeaving(extensions = {ValidationExtension.class})
@DefaultWeavingExtensions
@PojoWeavingExtension(ValidatorsPojoWeavingProcessor.class)
@CompoundWeaving
@Data
@AllArgsConstructor
@NoArgsConstructor
public static class Config {
@Validator(
value = WeavableNumberRangeValidator.class,
config = "0=..100"
)
private int value;
}
}

View File

@@ -0,0 +1,100 @@
package de.siphalor.tweed5.weaver.pojoext.validation.api.validators;
import de.siphalor.tweed5.core.api.entry.ConfigEntry;
import de.siphalor.tweed5.core.impl.DefaultConfigContainer;
import de.siphalor.tweed5.core.impl.entry.SimpleConfigEntryImpl;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import java.util.List;
import java.util.stream.Stream;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertAll;
import static org.junit.jupiter.params.provider.Arguments.argumentSet;
class WeavableNumberRangeValidatorTest {
@ParameterizedTest
@MethodSource("constructParams")
void construct(Class<? extends Number> numberClass, String config, List<Number> works, List<Number> fails) {
DefaultConfigContainer<Object> configContainer = new DefaultConfigContainer<>();
configContainer.finishExtensionSetup();
//noinspection unchecked
var configEntry = new SimpleConfigEntryImpl<>(configContainer, (Class<Number>) numberClass);
var validator = WeavableConfigEntryValidator.FACTORY.construct(WeavableNumberRangeValidator.class)
.typedArg(ConfigEntry.class, configEntry)
.namedArg("config", config)
.finish();
assertAll(Stream.concat(
works.stream().map(
number -> () -> assertThat(validator.validate(configEntry, number).issues())
.as("Number %s should not have validation issues", number)
.isEmpty()
),
fails.stream().map(
number -> () -> assertThat(validator.validate(configEntry, number).issues())
.as("Number %s should have validation issues", number)
.isNotEmpty()
)
));
}
static Stream<Arguments> constructParams() {
return Stream.of(
argumentSet(
"integer, no range",
Integer.class,
"..",
List.of(Integer.MIN_VALUE, -1234, 0, 1234, Integer.MAX_VALUE),
List.of()
),
argumentSet(
"integer, greater than",
Integer.class,
"12..",
List.of(13, 25, Integer.MAX_VALUE),
List.of(Integer.MIN_VALUE, -1234, 0, 12)
),
argumentSet(
"integer, greater than or equal",
Integer.class,
"12=..",
List.of(12, 13, 24, Integer.MAX_VALUE),
List.of(Integer.MIN_VALUE, -1234, 0, 11)
),
argumentSet(
"integer, less than",
Integer.class,
"..-12",
List.of(Integer.MIN_VALUE, -1234, -13),
List.of(-12, 0, 12, 1234, Integer.MAX_VALUE)
),
argumentSet(
"integer, less than or equal",
Integer.class,
"..=-12",
List.of(Integer.MIN_VALUE, -1234, -13, -12),
List.of(-11, 0, 12, 1234, Integer.MAX_VALUE)
),
argumentSet(
"integer, range",
Integer.class,
"0=..100",
List.of(0, 50, 99),
List.of(Integer.MIN_VALUE, -1, 100, Integer.MAX_VALUE)
),
argumentSet(
"double, range",
Double.class,
"1.5=..2.5",
List.of(1.5, 2.0, 2.1, 2.49999),
List.of(Integer.MIN_VALUE, 0, 1.49999, 2.5, 3.0, Integer.MAX_VALUE)
)
);
}
}