From 72f909833953120f7aad48f38b733d4b44a4e671 Mon Sep 17 00:00:00 2001 From: Roberto Cortez Date: Thu, 21 Jan 2021 00:31:19 +0000 Subject: [PATCH] Support validation in ConfigMappings. --- .../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 cv = new ClassValue() { @@ -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 { + public interface NamingStrategy extends Function { } 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, Map> mappings; - ConfigMappings() { + ConfigMappings(final ConfigValidator configValidator) { + this.configValidator = configValidator; this.mappings = new ConcurrentHashMap<>(); } @@ -76,11 +78,14 @@ T getConfigMapping(Class 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 interceptors = new ArrayList<>(); private final KeyMap 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 discoverSources() { List discoveredSources = new ArrayList<>(); ServiceLoader configSourceLoader = ServiceLoader.load(ConfigSource.class, classLoader); @@ -131,6 +139,15 @@ List discoverInterceptors() { return interceptors; } + ConfigValidator discoverValidator() { + ServiceLoader validatorLoader = ServiceLoader.load(ConfigValidator.class, classLoader); + Iterator 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 getInterceptors() { return interceptors; } + private ConfigValidator getValidator() { + if (isAddDiscoveredValidator()) { + this.validator = discoverValidator(); + } + return validator; + } + KeyMap 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 @@ common implementation cdi + validator sources/hocon sources/file-system sources/yaml 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 @@ + + + + 4.0.0 + + + io.smallrye.config + smallrye-config-parent + 2.2.1-SNAPSHOT + + + smallrye-config-validator + + SmallRye: MicroProfile Config Validator + + + 2.0.2 + + + 6.2.0.Final + 3.0.3 + + + + + io.smallrye.config + smallrye-config + + + jakarta.validation + jakarta.validation-api + ${version.jakarta.validation} + provided + + + + + org.junit.jupiter + junit-jupiter + + + org.hibernate.validator + hibernate-validator + ${version.hibernate.validator} + test + + + org.glassfish + jakarta.el + ${version.jakarta.el} + test + + + 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 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 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 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 problems) { + + try { + Set> violations = getValidator().forExecutables().validateReturnValue(mappingObject, + property.getMethod(), + property.getMethod().invoke(mappingObject)); + for (ConstraintViolation 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 problems) { + final Set> violations = getValidator().validate(mappingObject); + for (ConstraintViolation 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 properties = new HashMap<>(); + + private KeyValuesConfigSource(Map properties) { + this.properties.putAll(properties); + } + + @Override + public Map getProperties() { + return Collections.unmodifiableMap(properties); + } + + @Override + public Set 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 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 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 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 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 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 form(); + + Optional proxy(); + + Log log(); + + Cors cors(); + + Info info(); + + interface Proxy { + boolean enable(); + + @Max(10) + int timeout(); + } + + interface Log { + @Max(15) + int days(); + } + + interface Cors { + List 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> alias(); + + Map 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(); + } + } +}