39public final class ConfigValidator {
41 private ConfigValidator() {
44 public static void validate(
final Object instance) {
45 Objects.requireNonNull(instance,
"instance");
46 final List<ConfigViolation> violations =
new ArrayList<>();
47 validateInstance(instance,
"", violations);
48 if (!violations.isEmpty()) {
53 private static void validateInstance(
final Object instance,
final String path,
54 final List<ConfigViolation> violations) {
55 if (instance ==
null || !instance.getClass().isRecord()) {
59 for (
final RecordComponent component : instance.getClass().getRecordComponents()) {
60 final var componentPath = path.isBlank() ? component.getName() : path +
"." + component.getName();
63 final var accessor = component.getAccessor();
64 accessor.setAccessible(
true);
65 value = accessor.invoke(instance);
66 }
catch (
final ReflectiveOperationException e) {
67 throw new IllegalStateException(
"Unable to validate component " + componentPath, e);
70 if (component.isAnnotationPresent(Required.class) && value ==
null) {
78 final var notBlank = component.getAnnotation(NotBlank.class);
79 if (notBlank !=
null && value instanceof
final String stringValue && stringValue.isBlank()) {
80 violations.add(
new ConfigViolation(componentPath,
"must not be blank"));
83 final var min = component.getAnnotation(Min.class);
84 if (min !=
null && value instanceof
final Number number && number.longValue() < min.value()) {
85 violations.add(
new ConfigViolation(componentPath,
"must be >= " + min.value()));
88 final var max = component.getAnnotation(Max.class);
89 if (max !=
null && value instanceof
final Number number && number.longValue() > max.value()) {
90 violations.add(
new ConfigViolation(componentPath,
"must be <= " + max.value()));
93 final var pattern = component.getAnnotation(Pattern.class);
94 if (pattern !=
null && value instanceof
final String stringValue) {
96 if (!java.util.regex.Pattern.compile(pattern.value()).matcher(stringValue).matches()) {
97 violations.add(
new ConfigViolation(componentPath,
"must match " + pattern.value()));
99 }
catch (
final PatternSyntaxException e) {
100 throw new IllegalArgumentException(
"Invalid regex on " + componentPath +
": " + pattern.value(), e);
104 final var size = component.getAnnotation(Size.class);
106 final var actualSize = sizeOf(value);
107 if (actualSize >= 0 && (actualSize < size.min() || actualSize > size.max())) {
109 "size must be between " + size.min() +
" and " + size.max()));
113 if (component.isAnnotationPresent(Valid.class)) {
114 validateNested(value, componentPath, violations);
119 private static void validateNested(
final Object value,
final String path,
final List<ConfigViolation> violations) {
123 if (value.getClass().isRecord()) {
124 validateInstance(value, path, violations);
127 if (value instanceof
final Collection<?> collection) {
129 for (
final Object element : collection) {
130 validateNested(element, path +
"[" + index +
"]", violations);
135 if (value instanceof
final Map<?, ?> map) {
136 for (
final var entry : map.entrySet()) {
137 validateNested(entry.getValue(), path +
"." + entry.getKey(), violations);
142 private static int sizeOf(
final Object value) {
143 return switch (value) {
144 case final String stringValue -> stringValue.length();
145 case final Collection<?> collection -> collection.size();
146 case final Map<?, ?> map -> map.size();
147 case null,
default -> -1;