[build] Restructure to composite build
This commit is contained in:
9
tweed5/weaver-pojo-validation-extension/build.gradle.kts
Normal file
9
tweed5/weaver-pojo-validation-extension/build.gradle.kts
Normal 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"))
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
module.name = Validation Extension for Tweed 5 Weaver POJO
|
||||
module.description = Allows declaring validation constraints on POJOs using annotations.
|
||||
@@ -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 "";
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.weaver.pojoext.validation.api;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
@NullMarked
|
||||
package de.siphalor.tweed5.weaver.pojoext.validation.api.validators;
|
||||
|
||||
import org.jspecify.annotations.NullMarked;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user