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 private static final int MAX_DEPTH = 20;
45
46 public static void validate(final Object instance) {
47 Objects.requireNonNull(instance, "instance");
48 final List<ConfigViolation> violations = new ArrayList<>();
49 validateInstance(instance, "", violations, 0);
50 if (!violations.isEmpty()) {
51 throw new ConfigValidationException(violations);
52 }
53 }
54
55 private static void validateInstance(final Object instance, final String path,
56 final List<ConfigViolation> violations, final int depth) {
57 if (instance == null || !instance.getClass().isRecord()) {
58 return;
59 }
60
61 for (final RecordComponent component : instance.getClass().getRecordComponents()) {
62 final var componentPath = path.isBlank() ? component.getName() : path + "." + component.getName();
63 final Object value;
64 try {
65 final var accessor = component.getAccessor();
66 accessor.setAccessible(true);
67 value = accessor.invoke(instance);
68 } catch (final ReflectiveOperationException e) {
69 throw new IllegalStateException("Unable to validate component " + componentPath, e);
70 }
71
72 if (component.isAnnotationPresent(Required.class) && value == null) {
73 violations.add(new ConfigViolation(componentPath, "is required"));
74 continue;
75 }
76 if (value == null) {
77 continue;
78 }
79
80 final var notBlank = component.getAnnotation(NotBlank.class);
81 if (notBlank != null && value instanceof final String stringValue && stringValue.isBlank()) {
82 violations.add(new ConfigViolation(componentPath, "must not be blank"));
83 }
84
85 final var min = component.getAnnotation(Min.class);
86 if (min != null && value instanceof final Number number && number.longValue() < min.value()) {
87 violations.add(new ConfigViolation(componentPath, "must be >= " + min.value()));
88 }
89
90 final var max = component.getAnnotation(Max.class);
91 if (max != null && value instanceof final Number number && number.longValue() > max.value()) {
92 violations.add(new ConfigViolation(componentPath, "must be <= " + max.value()));
93 }
94
95 final var pattern = component.getAnnotation(Pattern.class);
96 if (pattern != null && value instanceof final String stringValue) {
97 try {
98 if (!java.util.regex.Pattern.compile(pattern.value()).matcher(stringValue).matches()) {
99 violations.add(new ConfigViolation(componentPath, "must match " + pattern.value()));
100 }
101 } catch (final PatternSyntaxException e) {
102 throw new IllegalArgumentException("Invalid regex on " + componentPath + ": " + pattern.value(), e);
103 }
104 }
105
106 final var size = component.getAnnotation(Size.class);
107 if (size != null) {
108 final var actualSize = sizeOf(value);
109 if (actualSize >= 0 && (actualSize < size.min() || actualSize > size.max())) {
110 violations.add(new ConfigViolation(componentPath,
111 "size must be between " + size.min() + " and " + size.max()));
112 }
113 }
114
115 if (component.isAnnotationPresent(Valid.class)) {
116 validateNested(value, componentPath, violations, depth + 1);
117 }
118 }
119 }
120
121 private static void validateNested(final Object value, final String path, final List<ConfigViolation> violations,
122 final int depth) {
123 if (depth >= MAX_DEPTH) {
124 return;
125 }
126 if (value == null) {
127 return;
128 }
129 if (value.getClass().isRecord()) {
130 validateInstance(value, path, violations, depth + 1);
131 return;
132 }
133 if (value instanceof final Collection<?> collection) {
134 var index = 0;
135 for (final Object element : collection) {
136 validateNested(element, path + "[" + index + "]", violations, depth + 1);
137 index++;
138 }
139 return;
140 }
141 if (value instanceof final Map<?, ?> map) {
142 for (final var entry : map.entrySet()) {
143 validateNested(entry.getValue(), path + "." + entry.getKey(), violations, depth + 1);
144 }
145 }
146 }
147
148 private static int sizeOf(final Object value) {
149 return switch (value) {
150 case final String stringValue -> stringValue.length();
151 case final Collection<?> collection -> collection.size();
152 case final Map<?, ?> map -> map.size();
153 case null, default -> -1;
154 };
155 }
156}
static void validate(final Object instance)
record ConfigViolation(String path, String message)