From b10181c9572a66b78bb4de39bb4cd49cfaf85af3 Mon Sep 17 00:00:00 2001
From: Roberto Cortez <radcortez@yahoo.com>
Date: Fri, 7 May 2021 13:10:24 +0100
Subject: [PATCH] Support validation in ConfigMappings. (#488)

---
 .../config/ConfigMappingInterface.java        |  10 +-
 .../io/smallrye/config/ConfigMappings.java    |  11 +-
 .../config/ConfigValidationException.java     |   2 +-
 .../io/smallrye/config/ConfigValidator.java   |   8 +
 .../config/SmallRyeConfigBuilder.java         |  35 ++-
 pom.xml                                       |   1 +
 validator/pom.xml                             |  68 ++++++
 .../BeanValidationConfigValidator.java        | 196 ++++++++++++++++
 .../BeanValidationConfigValidatorImpl.java    |  17 ++
 .../validator/KeyValuesConfigSource.java      |  54 +++++
 .../config/validator/ValidateConfigTest.java  | 212 ++++++++++++++++++
 11 files changed, 604 insertions(+), 10 deletions(-)
 create mode 100644 implementation/src/main/java/io/smallrye/config/ConfigValidator.java
 create mode 100644 validator/pom.xml
 create mode 100644 validator/src/main/java/io/smallrye/config/validator/BeanValidationConfigValidator.java
 create mode 100644 validator/src/main/java/io/smallrye/config/validator/BeanValidationConfigValidatorImpl.java
 create mode 100644 validator/src/test/java/io/smallrye/config/validator/KeyValuesConfigSource.java
 create mode 100644 validator/src/test/java/io/smallrye/config/validator/ValidateConfigTest.java

diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java b/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java
index 1f2d0890d..75c4c67fe 100644
--- a/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java
+++ b/implementation/src/main/java/io/smallrye/config/ConfigMappingInterface.java
@@ -24,7 +24,7 @@
 /**
  * Information about a configuration interface.
  */
-final class ConfigMappingInterface implements ConfigMappingMetadata {
+public final class ConfigMappingInterface implements ConfigMappingMetadata {
     static final ConfigMappingInterface[] NO_TYPES = new ConfigMappingInterface[0];
     static final Property[] NO_PROPERTIES = new Property[0];
     static final ClassValue<ConfigMappingInterface> cv = new ClassValue<ConfigMappingInterface>() {
@@ -56,7 +56,7 @@ protected ConfigMappingInterface computeValue(final Class<?> type) {
      * @param interfaceType the interface type (must not be {@code null})
      * @return the configuration interface, or {@code null} if the type does not appear to be a configuration interface
      */
-    static ConfigMappingInterface getConfigurationInterface(Class<?> interfaceType) {
+    public static ConfigMappingInterface getConfigurationInterface(Class<?> interfaceType) {
         Assert.checkNotNullParam("interfaceType", interfaceType);
         return cv.get(interfaceType);
     }
@@ -99,7 +99,7 @@ ConfigMappingInterface getSuperType(int index) throws IndexOutOfBoundsException
         return superTypes[index];
     }
 
-    Property[] getProperties() {
+    public Property[] getProperties() {
         return properties;
     }
 
@@ -130,7 +130,7 @@ Property getProperty(final String name) {
         return propertiesByName.get(name);
     }
 
-    NamingStrategy getNamingStrategy() {
+    public NamingStrategy getNamingStrategy() {
         return namingStrategy;
     }
 
@@ -792,7 +792,7 @@ private static NamingStrategy getNamingStrategy(final Class<?> interfaceType) {
     private static final NamingStrategy KEBAB_CASE_NAMING_STRATEGY = new KebabNamingStrategy();
     private static final NamingStrategy SNAKE_CASE_NAMING_STRATEGY = new SnakeNamingStrategy();
 
-    interface NamingStrategy extends Function<String, String> {
+    public interface NamingStrategy extends Function<String, String> {
 
     }
 
diff --git a/implementation/src/main/java/io/smallrye/config/ConfigMappings.java b/implementation/src/main/java/io/smallrye/config/ConfigMappings.java
index 359326159..33eb5d37b 100644
--- a/implementation/src/main/java/io/smallrye/config/ConfigMappings.java
+++ b/implementation/src/main/java/io/smallrye/config/ConfigMappings.java
@@ -16,9 +16,11 @@
 public final class ConfigMappings implements Serializable {
     private static final long serialVersionUID = -7790784345796818526L;
 
+    private final ConfigValidator configValidator;
     private final ConcurrentMap<Class<?>, Map<String, ConfigMappingObject>> mappings;
 
-    ConfigMappings() {
+    ConfigMappings(final ConfigValidator configValidator) {
+        this.configValidator = configValidator;
         this.mappings = new ConcurrentHashMap<>();
     }
 
@@ -76,11 +78,14 @@ <T> T getConfigMapping(Class<T> type, String prefix) {
             throw ConfigMessages.msg.mappingPrefixNotFound(type.getName(), prefix);
         }
 
+        Object value = configMappingObject;
         if (configMappingObject instanceof ConfigMappingClassMapper) {
-            return type.cast(((ConfigMappingClassMapper) configMappingObject).map());
+            value = ((ConfigMappingClassMapper) configMappingObject).map();
         }
 
-        return type.cast(configMappingObject);
+        configValidator.validateMapping(type, prefix, value);
+
+        return type.cast(value);
     }
 
     static String getPrefix(Class<?> type) {
diff --git a/implementation/src/main/java/io/smallrye/config/ConfigValidationException.java b/implementation/src/main/java/io/smallrye/config/ConfigValidationException.java
index 904c7f07e..bcd8957fb 100644
--- a/implementation/src/main/java/io/smallrye/config/ConfigValidationException.java
+++ b/implementation/src/main/java/io/smallrye/config/ConfigValidationException.java
@@ -7,7 +7,7 @@
 /**
  * An exception which is thrown when a configuration validation problem occurs.
  */
-public class ConfigValidationException extends Exception {
+public class ConfigValidationException extends RuntimeException {
     private static final long serialVersionUID = -2637730579475070264L;
 
     private final Problem[] problems;
diff --git a/implementation/src/main/java/io/smallrye/config/ConfigValidator.java b/implementation/src/main/java/io/smallrye/config/ConfigValidator.java
new file mode 100644
index 000000000..e7c0c998f
--- /dev/null
+++ b/implementation/src/main/java/io/smallrye/config/ConfigValidator.java
@@ -0,0 +1,8 @@
+package io.smallrye.config;
+
+public interface ConfigValidator {
+    void validateMapping(Class<?> mappingClass, String prefix, Object mappingObject) throws ConfigValidationException;
+
+    ConfigValidator EMPTY = (mappingClass, prefix, mappingObject) -> {
+    };
+}
diff --git a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java
index 6725cca37..00f94caff 100644
--- a/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java
+++ b/implementation/src/main/java/io/smallrye/config/SmallRyeConfigBuilder.java
@@ -24,6 +24,7 @@
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
 import java.util.OptionalInt;
@@ -54,12 +55,14 @@ public class SmallRyeConfigBuilder implements ConfigBuilder {
     private final List<InterceptorWithPriority> interceptors = new ArrayList<>();
     private final KeyMap<String> defaultValues = new KeyMap<>();
     private final ConfigMappingProvider.Builder mappingsBuilder = ConfigMappingProvider.builder();
+    private ConfigValidator validator = ConfigValidator.EMPTY;
     private ClassLoader classLoader = SecuritySupport.getContextClassLoader();
     private boolean addDefaultSources = false;
     private boolean addDefaultInterceptors = false;
     private boolean addDiscoveredSources = false;
     private boolean addDiscoveredConverters = false;
     private boolean addDiscoveredInterceptors = false;
+    private boolean addDiscoveredValidator = false;
 
     public SmallRyeConfigBuilder() {
     }
@@ -81,6 +84,11 @@ public SmallRyeConfigBuilder addDiscoveredInterceptors() {
         return this;
     }
 
+    public SmallRyeConfigBuilder addDiscoveredValidator() {
+        addDiscoveredValidator = true;
+        return this;
+    }
+
     List<ConfigSource> discoverSources() {
         List<ConfigSource> discoveredSources = new ArrayList<>();
         ServiceLoader<ConfigSource> configSourceLoader = ServiceLoader.load(ConfigSource.class, classLoader);
@@ -131,6 +139,15 @@ List<InterceptorWithPriority> discoverInterceptors() {
         return interceptors;
     }
 
+    ConfigValidator discoverValidator() {
+        ServiceLoader<ConfigValidator> validatorLoader = ServiceLoader.load(ConfigValidator.class, classLoader);
+        Iterator<ConfigValidator> iterator = validatorLoader.iterator();
+        if (iterator.hasNext()) {
+            return iterator.next();
+        }
+        return ConfigValidator.EMPTY;
+    }
+
     @Override
     public SmallRyeConfigBuilder addDefaultSources() {
         addDefaultSources = true;
@@ -283,6 +300,11 @@ public SmallRyeConfigBuilder withValidateUnknown(boolean validateUnknown) {
         return this;
     }
 
+    public SmallRyeConfigBuilder withValidator(ConfigValidator validator) {
+        this.validator = validator;
+        return this;
+    }
+
     @Override
     public SmallRyeConfigBuilder withConverters(Converter<?>[] converters) {
         for (Converter<?> converter : converters) {
@@ -335,6 +357,13 @@ List<InterceptorWithPriority> getInterceptors() {
         return interceptors;
     }
 
+    private ConfigValidator getValidator() {
+        if (isAddDiscoveredValidator()) {
+            this.validator = discoverValidator();
+        }
+        return validator;
+    }
+
     KeyMap<String> getDefaultValues() {
         return defaultValues;
     }
@@ -359,13 +388,17 @@ boolean isAddDiscoveredInterceptors() {
         return addDiscoveredInterceptors;
     }
 
+    boolean isAddDiscoveredValidator() {
+        return addDiscoveredValidator;
+    }
+
     @Override
     public SmallRyeConfig build() {
         ConfigMappingProvider mappingProvider = mappingsBuilder.build();
         defaultValues.putAll(mappingProvider.getDefaultValues());
 
         try {
-            ConfigMappings configMappings = new ConfigMappings();
+            ConfigMappings configMappings = new ConfigMappings(getValidator());
             SmallRyeConfig config = new SmallRyeConfig(this, configMappings);
             mappingProvider.mapConfiguration(config);
             return config;
diff --git a/pom.xml b/pom.xml
index 5b1a710e9..d71ab1022 100644
--- a/pom.xml
+++ b/pom.xml
@@ -67,6 +67,7 @@
     <module>common</module>
     <module>implementation</module>
     <module>cdi</module>
+    <module>validator</module>
     <module>sources/hocon</module>
     <module>sources/file-system</module>
     <module>sources/yaml</module>
diff --git a/validator/pom.xml b/validator/pom.xml
new file mode 100644
index 000000000..c24c849f9
--- /dev/null
+++ b/validator/pom.xml
@@ -0,0 +1,68 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ ~  Copyright 2021 Red Hat, Inc.
+ ~
+ ~  Licensed under the Apache License, Version 2.0 (the "License");
+ ~  you may not use this file except in compliance with the License.
+ ~  You may obtain a copy of the License at
+ ~
+ ~    http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~  Unless required by applicable law or agreed to in writing, software
+ ~  distributed under the License is distributed on an "AS IS" BASIS,
+ ~  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~  See the License for the specific language governing permissions and
+ ~  limitations under the License.
+  -->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>io.smallrye.config</groupId>
+        <artifactId>smallrye-config-parent</artifactId>
+        <version>2.2.1-SNAPSHOT</version>
+    </parent>
+
+    <artifactId>smallrye-config-validator</artifactId>
+
+    <name>SmallRye: MicroProfile Config Validator</name>
+
+    <properties>
+        <version.jakarta.validation>2.0.2</version.jakarta.validation>
+
+        <!-- Test -->
+        <version.hibernate.validator>6.2.0.Final</version.hibernate.validator>
+        <version.jakarta.el>3.0.3</version.jakarta.el>
+    </properties>
+
+    <dependencies>
+        <dependency>
+            <groupId>io.smallrye.config</groupId>
+            <artifactId>smallrye-config</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>jakarta.validation</groupId>
+            <artifactId>jakarta.validation-api</artifactId>
+            <version>${version.jakarta.validation}</version>
+            <scope>provided</scope>
+        </dependency>
+
+        <!-- Test -->
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.hibernate.validator</groupId>
+            <artifactId>hibernate-validator</artifactId>
+            <version>${version.hibernate.validator}</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.glassfish</groupId>
+            <artifactId>jakarta.el</artifactId>
+            <version>${version.jakarta.el}</version>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/validator/src/main/java/io/smallrye/config/validator/BeanValidationConfigValidator.java b/validator/src/main/java/io/smallrye/config/validator/BeanValidationConfigValidator.java
new file mode 100644
index 000000000..67583ac5f
--- /dev/null
+++ b/validator/src/main/java/io/smallrye/config/validator/BeanValidationConfigValidator.java
@@ -0,0 +1,196 @@
+package io.smallrye.config.validator;
+
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+
+import javax.validation.ConstraintViolation;
+import javax.validation.Path;
+import javax.validation.Validator;
+
+import io.smallrye.config.ConfigMappingInterface;
+import io.smallrye.config.ConfigMappingInterface.CollectionProperty;
+import io.smallrye.config.ConfigMappingInterface.MapProperty;
+import io.smallrye.config.ConfigMappingInterface.NamingStrategy;
+import io.smallrye.config.ConfigMappingInterface.Property;
+import io.smallrye.config.ConfigValidationException;
+import io.smallrye.config.ConfigValidationException.Problem;
+import io.smallrye.config.ConfigValidator;
+
+public interface BeanValidationConfigValidator extends ConfigValidator {
+
+    Validator getValidator();
+
+    @Override
+    default void validateMapping(
+            final Class<?> mappingClass,
+            final String prefix,
+            final Object mappingObject)
+            throws ConfigValidationException {
+
+        final List<Problem> problems = new ArrayList<>();
+        final ConfigMappingInterface mappingInterface = ConfigMappingInterface.getConfigurationInterface(mappingClass);
+        if (mappingInterface != null) {
+            validateMappingInterface(mappingInterface, prefix, mappingInterface.getNamingStrategy(), mappingObject, problems);
+        } else {
+            validateMappingClass(mappingObject, problems);
+        }
+
+        if (!problems.isEmpty()) {
+            throw new ConfigValidationException(problems.toArray(ConfigValidationException.Problem.NO_PROBLEMS));
+        }
+    }
+
+    default void validateMappingInterface(
+            final ConfigMappingInterface mappingInterface,
+            final String currentPath,
+            final NamingStrategy namingStrategy,
+            final Object mappingObject,
+            final List<Problem> problems) {
+
+        for (Property property : mappingInterface.getProperties()) {
+            validateProperty(property, currentPath, namingStrategy, mappingObject, false, problems);
+        }
+    }
+
+    default void validateProperty(
+            final Property property,
+            final String currentPath,
+            final NamingStrategy namingStrategy,
+            final Object mappingObject,
+            final boolean optional,
+            final List<Problem> problems) {
+
+        if (property.isOptional()) {
+            validateProperty(property.asOptional().getNestedProperty(), currentPath, namingStrategy, mappingObject, true,
+                    problems);
+        }
+
+        if ((property.isLeaf() || property.isPrimitive()) && !property.isOptional()) {
+            validatePropertyValue(property, currentPath, namingStrategy, mappingObject, problems);
+        }
+
+        if (property.isGroup()) {
+            try {
+                Object group = property.getMethod().invoke(mappingObject);
+                // unwrap
+                if (optional) {
+                    Optional<?> optionalGroup = (Optional<?>) group;
+                    if (!optionalGroup.isPresent()) {
+                        return;
+                    }
+                    group = optionalGroup.get();
+                }
+
+                validateMappingInterface(property.asGroup().getGroupType(), appendPropertyName(currentPath, property),
+                        namingStrategy, group, problems);
+            } catch (IllegalAccessException | InvocationTargetException e) {
+                e.printStackTrace();
+            }
+        }
+
+        if (property.isCollection()) {
+            CollectionProperty collectionProperty = property.asCollection();
+            if (collectionProperty.getElement().isGroup()) {
+                try {
+                    Collection<?> collection = (Collection<?>) property.getMethod().invoke(mappingObject);
+                    int i = 0;
+                    for (Object element : collection) {
+                        validateMappingInterface(collectionProperty.getElement().asGroup().getGroupType(),
+                                appendPropertyName(currentPath, property) + "[" + i + "]",
+                                namingStrategy, element, problems);
+                        i++;
+                    }
+                } catch (IllegalAccessException | InvocationTargetException e) {
+                    e.printStackTrace();
+                }
+            } else if (collectionProperty.getElement().isLeaf()) {
+                validateProperty(collectionProperty.getElement(), currentPath, namingStrategy, mappingObject, optional,
+                        problems);
+            }
+        }
+
+        if (property.isMap()) {
+            MapProperty mapProperty = property.asMap();
+            if (mapProperty.getValueProperty().isGroup()) {
+                try {
+                    Map<?, ?> map = (Map<?, ?>) property.getMethod().invoke(mappingObject);
+                    for (Map.Entry<?, ?> entry : map.entrySet()) {
+                        validateMappingInterface(mapProperty.getValueProperty().asGroup().getGroupType(),
+                                appendPropertyName(currentPath, property) + "." + entry.getKey(),
+                                namingStrategy, entry.getValue(), problems);
+                    }
+                } catch (IllegalAccessException | InvocationTargetException e) {
+                    e.printStackTrace();
+                }
+            } else if (mapProperty.getValueProperty().isLeaf()) {
+                validatePropertyValue(property, currentPath, namingStrategy, mappingObject, problems);
+            }
+        }
+    }
+
+    default void validatePropertyValue(
+            final Property property,
+            final String currentPath,
+            final NamingStrategy namingStrategy,
+            final Object mappingObject,
+            final List<Problem> problems) {
+
+        try {
+            Set<ConstraintViolation<Object>> violations = getValidator().forExecutables().validateReturnValue(mappingObject,
+                    property.getMethod(),
+                    property.getMethod().invoke(mappingObject));
+            for (ConstraintViolation<Object> violation : violations) {
+                problems.add(new Problem(interpolateMessage(currentPath, namingStrategy, property, violation)));
+            }
+        } catch (IllegalArgumentException | IllegalAccessException | InvocationTargetException e) {
+            e.printStackTrace();
+        }
+    }
+
+    default void validateMappingClass(final Object mappingObject, final List<Problem> problems) {
+        final Set<ConstraintViolation<Object>> violations = getValidator().validate(mappingObject);
+        for (ConstraintViolation<Object> violation : violations) {
+            problems.add(new Problem(violation.getPropertyPath() + " " + violation.getMessage()));
+        }
+    }
+
+    default String appendPropertyName(final String currentPath, final Property property) {
+        if (currentPath.isEmpty()) {
+            return property.getPropertyName();
+        }
+
+        if (property.getPropertyName().isEmpty()) {
+            return currentPath;
+        }
+
+        return currentPath + "." + property.getPropertyName();
+    }
+
+    default String interpolateMessage(
+            final String currentPath,
+            final NamingStrategy namingStrategy,
+            final Property property,
+            final ConstraintViolation<?> violation) {
+        StringBuilder propertyName = new StringBuilder(currentPath);
+        String name = namingStrategy.apply(property.getPropertyName());
+        if (!name.isEmpty()) {
+            propertyName.append(".").append(name);
+        }
+        Path propertyPath = violation.getPropertyPath();
+        for (Path.Node node : propertyPath) {
+            if (node.isInIterable()) {
+                if (node.getIndex() != null) {
+                    propertyName.append("[").append(node.getIndex()).append("]");
+                } else if (node.getKey() != null) {
+                    propertyName.append(".").append(node.getKey());
+                }
+            }
+        }
+        return propertyName.toString() + " " + violation.getMessage();
+    }
+}
diff --git a/validator/src/main/java/io/smallrye/config/validator/BeanValidationConfigValidatorImpl.java b/validator/src/main/java/io/smallrye/config/validator/BeanValidationConfigValidatorImpl.java
new file mode 100644
index 000000000..54aa1ac8a
--- /dev/null
+++ b/validator/src/main/java/io/smallrye/config/validator/BeanValidationConfigValidatorImpl.java
@@ -0,0 +1,17 @@
+package io.smallrye.config.validator;
+
+import javax.validation.Validation;
+import javax.validation.Validator;
+
+public class BeanValidationConfigValidatorImpl implements BeanValidationConfigValidator {
+    private Validator validator;
+
+    public BeanValidationConfigValidatorImpl() {
+        this.validator = Validation.buildDefaultValidatorFactory().getValidator();
+    }
+
+    @Override
+    public Validator getValidator() {
+        return validator;
+    }
+}
diff --git a/validator/src/test/java/io/smallrye/config/validator/KeyValuesConfigSource.java b/validator/src/test/java/io/smallrye/config/validator/KeyValuesConfigSource.java
new file mode 100644
index 000000000..2f6cb36f4
--- /dev/null
+++ b/validator/src/test/java/io/smallrye/config/validator/KeyValuesConfigSource.java
@@ -0,0 +1,54 @@
+package io.smallrye.config.validator;
+
+import java.io.Serializable;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+
+import org.eclipse.microprofile.config.spi.ConfigSource;
+
+public class KeyValuesConfigSource implements ConfigSource, Serializable {
+
+    private final Map<String, String> properties = new HashMap<>();
+
+    private KeyValuesConfigSource(Map<String, String> properties) {
+        this.properties.putAll(properties);
+    }
+
+    @Override
+    public Map<String, String> getProperties() {
+        return Collections.unmodifiableMap(properties);
+    }
+
+    @Override
+    public Set<String> getPropertyNames() {
+        return Collections.unmodifiableSet(properties.keySet());
+    }
+
+    @Override
+    public String getValue(String propertyName) {
+        return properties.get(propertyName);
+    }
+
+    @Override
+    public String getName() {
+        return "KeyValuesConfigSource";
+    }
+
+    public static ConfigSource config(Map<String, String> properties) {
+        return new KeyValuesConfigSource(properties);
+    }
+
+    public static ConfigSource config(String... keyValues) {
+        if (keyValues.length % 2 != 0) {
+            throw new IllegalArgumentException("keyValues array must be a multiple of 2");
+        }
+
+        Map<String, String> props = new HashMap<>();
+        for (int i = 0; i < keyValues.length; i += 2) {
+            props.put(keyValues[i], keyValues[i + 1]);
+        }
+        return new KeyValuesConfigSource(props);
+    }
+}
diff --git a/validator/src/test/java/io/smallrye/config/validator/ValidateConfigTest.java b/validator/src/test/java/io/smallrye/config/validator/ValidateConfigTest.java
new file mode 100644
index 000000000..4a6aaf165
--- /dev/null
+++ b/validator/src/test/java/io/smallrye/config/validator/ValidateConfigTest.java
@@ -0,0 +1,212 @@
+package io.smallrye.config.validator;
+
+import static io.smallrye.config.validator.KeyValuesConfigSource.config;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.OptionalInt;
+
+import javax.validation.constraints.Max;
+import javax.validation.constraints.Min;
+import javax.validation.constraints.Size;
+
+import org.eclipse.microprofile.config.inject.ConfigProperties;
+import org.junit.jupiter.api.Test;
+
+import io.smallrye.config.ConfigMapping;
+import io.smallrye.config.ConfigValidationException;
+import io.smallrye.config.SmallRyeConfig;
+import io.smallrye.config.SmallRyeConfigBuilder;
+import io.smallrye.config.WithParentName;
+
+public class ValidateConfigTest {
+    @Test
+    void validateConfigMapping() {
+        SmallRyeConfig config = new SmallRyeConfigBuilder()
+                .withValidator(new BeanValidationConfigValidatorImpl())
+                .withSources(config(
+                        "server.host", "localhost",
+                        "server.port", "8080",
+                        "server.log.days", "20",
+                        "server.proxy.enable", "true",
+                        "server.proxy.timeout", "20",
+                        "server.form.login-page", "login.html",
+                        "server.form.error-page", "error.html",
+                        "server.form.landing-page", "index.html",
+                        "server.cors.origins[0].host", "some-server",
+                        "server.cors.origins[0].port", "9000",
+                        "server.cors.origins[1].host", "localhost",
+                        "server.cors.origins[1].port", "1",
+                        "server.cors.methods[0]", "GET",
+                        "server.cors.methods[1]", "POST",
+                        "server.info.name", "Bond",
+                        "server.info.code", "007",
+                        "server.info.alias[0]", "James",
+                        "server.info.admins.root.username", "root"))
+                .withMapping(Server.class, "server")
+                .build();
+
+        ConfigValidationException validationException = assertThrows(ConfigValidationException.class,
+                () -> config.getConfigMapping(Server.class, "server"));
+        List<String> validations = new ArrayList<>();
+        for (int i = 0; i < validationException.getProblemCount(); i++) {
+            validations.add(validationException.getProblem(i).getMessage());
+        }
+        assertTrue(validations.contains("server.port must be less than or equal to 10"));
+        assertTrue(validations.contains("server.log.days must be less than or equal to 15"));
+        assertTrue(validations.contains("server.proxy.timeout must be less than or equal to 10"));
+        assertTrue(validations.contains("server.cors.origins[0].host size must be between 0 and 10"));
+        assertTrue(validations.contains("server.cors.origins[0].port must be less than or equal to 10"));
+        assertTrue(validations.contains("server.cors.methods[1] size must be between 0 and 3"));
+        assertTrue(validations.contains("server.form.login-page size must be between 0 and 3"));
+        assertTrue(validations.contains("server.form.error-page size must be between 0 and 3"));
+        assertTrue(validations.contains("server.form.landing-page size must be between 0 and 3"));
+        assertTrue(validations.contains("server.info.name size must be between 0 and 3"));
+        assertTrue(validations.contains("server.info.code must be less than or equal to 3"));
+        assertTrue(validations.contains("server.info.alias[0] size must be between 0 and 3"));
+        assertTrue(validations.contains("server.info.admins.root.username size must be between 0 and 3"));
+    }
+
+    @Test
+    void validateNamingStrategy() {
+        SmallRyeConfig config = new SmallRyeConfigBuilder()
+                .withValidator(new BeanValidationConfigValidatorImpl())
+                .withSources(config(
+                        "server.the_host", "localhost",
+                        "server.the_port", "8080"))
+                .withMapping(ServerNamingStrategy.class, "server")
+                .build();
+
+        ConfigValidationException validationException = assertThrows(ConfigValidationException.class,
+                () -> config.getConfigMapping(ServerNamingStrategy.class, "server"));
+        List<String> validations = new ArrayList<>();
+        for (int i = 0; i < validationException.getProblemCount(); i++) {
+            validations.add(validationException.getProblem(i).getMessage());
+        }
+
+        assertTrue(validations.contains("server.the_port must be less than or equal to 10"));
+    }
+
+    @Test
+    void validateConfigProperties() {
+        SmallRyeConfig config = new SmallRyeConfigBuilder()
+                .withValidator(new BeanValidationConfigValidatorImpl())
+                .withMapping(Client.class, "client")
+                .build();
+
+        ConfigValidationException validationException = assertThrows(ConfigValidationException.class,
+                () -> config.getConfigMapping(Client.class, "client"));
+        assertEquals(1, validationException.getProblemCount());
+        List<String> validations = new ArrayList<>();
+        validations.add(validationException.getProblem(0).getMessage());
+        assertTrue(validations.contains("port must be less than or equal to 10"));
+    }
+
+    @Test
+    void validateParent() {
+        SmallRyeConfig config = new SmallRyeConfigBuilder()
+                .withValidator(new BeanValidationConfigValidatorImpl())
+                .withSources(config(
+                        "server.host", "localhost",
+                        "server.port", "80"))
+                .withMapping(ServerParent.class, "server")
+                .build();
+
+        ConfigValidationException validationException = assertThrows(ConfigValidationException.class,
+                () -> config.getConfigMapping(ServerParent.class, "server"));
+        assertEquals("server.port must be greater than or equal to 8000", validationException.getProblem(0).getMessage());
+    }
+
+    @ConfigMapping(prefix = "server")
+    public interface Server {
+        String host();
+
+        @Max(10)
+        int port();
+
+        Map<String, @Size(max = 3) String> form();
+
+        Optional<Proxy> proxy();
+
+        Log log();
+
+        Cors cors();
+
+        Info info();
+
+        interface Proxy {
+            boolean enable();
+
+            @Max(10)
+            int timeout();
+        }
+
+        interface Log {
+            @Max(15)
+            int days();
+        }
+
+        interface Cors {
+            List<Origin> origins();
+
+            List<@Size(max = 3) String> methods();
+
+            interface Origin {
+                @Size(max = 10)
+                String host();
+
+                @Max(10)
+                int port();
+            }
+        }
+
+        interface Info {
+            Optional<@Size(max = 3) String> name();
+
+            @Max(3)
+            OptionalInt code();
+
+            Optional<List<@Size(max = 3) String>> alias();
+
+            Map<String, Admin> admins();
+
+            interface Admin {
+                @Size(max = 3)
+                String username();
+            }
+        }
+    }
+
+    @ConfigMapping(prefix = "server", namingStrategy = ConfigMapping.NamingStrategy.SNAKE_CASE)
+    public interface ServerNamingStrategy {
+        String theHost();
+
+        @Max(10)
+        int thePort();
+    }
+
+    @ConfigProperties(prefix = "client")
+    public static class Client {
+        public String host = "localhost";
+        @Max(10)
+        public int port = 8080;
+    }
+
+    @ConfigMapping(prefix = "server")
+    public interface ServerParent {
+        @WithParentName
+        Parent parent();
+
+        interface Parent {
+            String host();
+
+            @Min(8000)
+            int port();
+        }
+    }
+}