Ether Framework
Unified API docs for Ether modules
Loading...
Searching...
No Matches
ConfigValidator.java
Go to the documentation of this file.
1package dev.rafex.ether.config.validation;
2
3/*-
4 * #%L
5 * ether-config
6 * %%
7 * Copyright (C) 2025 - 2026 Raúl Eduardo González Argote
8 * %%
9 * Permission is hereby granted, free of charge, to any person obtaining a copy
10 * of this software and associated documentation files (the "Software"), to deal
11 * in the Software without restriction, including without limitation the rights
12 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
13 * copies of the Software, and to permit persons to whom the Software is
14 * furnished to do so, subject to the following conditions:
15 *
16 * The above copyright notice and this permission notice shall be included in
17 * all copies or substantial portions of the Software.
18 *
19 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
20 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
21 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
22 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
23 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
24 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
25 * THE SOFTWARE.
26 * #L%
27 */
28
29import java.lang.reflect.RecordComponent;
30import java.util.ArrayList;
31import java.util.Collection;
32import java.util.List;
33import java.util.Map;
34import java.util.Objects;
35import java.util.regex.PatternSyntaxException;
36
37import dev.rafex.ether.config.exceptions.ConfigValidationException;
38
39public final class ConfigValidator {
40
41 private ConfigValidator() {
42 }
43
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()) {
49 throw new ConfigValidationException(violations);
50 }
51 }
52
53 private static void validateInstance(final Object instance, final String path,
54 final List<ConfigViolation> violations) {
55 if (instance == null || !instance.getClass().isRecord()) {
56 return;
57 }
58
59 for (final RecordComponent component : instance.getClass().getRecordComponents()) {
60 final var componentPath = path.isBlank() ? component.getName() : path + "." + component.getName();
61 final Object value;
62 try {
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);
68 }
69
70 if (component.isAnnotationPresent(Required.class) && value == null) {
71 violations.add(new ConfigViolation(componentPath, "is required"));
72 continue;
73 }
74 if (value == null) {
75 continue;
76 }
77
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"));
81 }
82
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()));
86 }
87
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()));
91 }
92
93 final var pattern = component.getAnnotation(Pattern.class);
94 if (pattern != null && value instanceof final String stringValue) {
95 try {
96 if (!java.util.regex.Pattern.compile(pattern.value()).matcher(stringValue).matches()) {
97 violations.add(new ConfigViolation(componentPath, "must match " + pattern.value()));
98 }
99 } catch (final PatternSyntaxException e) {
100 throw new IllegalArgumentException("Invalid regex on " + componentPath + ": " + pattern.value(), e);
101 }
102 }
103
104 final var size = component.getAnnotation(Size.class);
105 if (size != null) {
106 final var actualSize = sizeOf(value);
107 if (actualSize >= 0 && (actualSize < size.min() || actualSize > size.max())) {
108 violations.add(new ConfigViolation(componentPath,
109 "size must be between " + size.min() + " and " + size.max()));
110 }
111 }
112
113 if (component.isAnnotationPresent(Valid.class)) {
114 validateNested(value, componentPath, violations);
115 }
116 }
117 }
118
119 private static void validateNested(final Object value, final String path, final List<ConfigViolation> violations) {
120 if (value == null) {
121 return;
122 }
123 if (value.getClass().isRecord()) {
124 validateInstance(value, path, violations);
125 return;
126 }
127 if (value instanceof final Collection<?> collection) {
128 var index = 0;
129 for (final Object element : collection) {
130 validateNested(element, path + "[" + index + "]", violations);
131 index++;
132 }
133 return;
134 }
135 if (value instanceof final Map<?, ?> map) {
136 for (final var entry : map.entrySet()) {
137 validateNested(entry.getValue(), path + "." + entry.getKey(), violations);
138 }
139 }
140 }
141
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;
148 };
149 }
150}
static void validate(final Object instance)
record ConfigViolation(String path, String message)