diff --git a/README.md b/README.md index fae82e815..0e352e6b3 100644 --- a/README.md +++ b/README.md @@ -994,7 +994,7 @@ By default, the builder has several rules predefined [here](https://github.com/g # Additional Modules ## Micrometer Metrics -Gestalt exposes several metrics and provides a implementation for micrometer. +Gestalt exposes several metrics and provides a implementation for [micrometer](https://micrometer.io/). To import the micrometer implementation add `gestalt-micrometer` to your build files. @@ -1013,13 +1013,10 @@ implementation("com.github.gestalt-config:gestalt-micrometer:${version}") Then when building gestalt, you need to register the module config `MicrometerModuleConfig` using the `MicrometerModuleConfigBuilder`. -An example of using the registering the `MicrometerModuleConfig` using the `MicrometerModuleConfigBuilder`. - ```java SimpleMeterRegistry registry = new SimpleMeterRegistry(); -GestaltBuilder builder = new GestaltBuilder(); -Gestalt gestalt = builder +Gestalt gestalt = new GestaltBuilder() .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) .setMetricsEnabled(true) .addModuleConfig(MicrometerModuleConfigBuilder.builder() @@ -1054,6 +1051,47 @@ The following metrics are exposed | cache.hit | Incremented for each request served from the cache. A cache miss would be recorded in the metric config.get | Counter | | +## Hibernate Validator +Gestalt allows a validator to hook into and validate calls to get a configuration object. Gestalt includes a [Hibernate Bean Validator](https://hibernate.org/validator/) implementation. + +If the object decoded fails to validate, a `GestaltException` is thrown with the details of the failed validations. +For calls to `getConfig` with a default value it will log the failed validations then return the default value. +For calls to `getConfigOptional` it will log the failed validations then return an `Optional.empty()`. + +To import the Hibernate Validator implementation add `gestalt-validator-hibernate` to your build files. + +In Maven: +```xml + + com.github.gestalt-config + gestalt-validator-hibernate + ${version} + +``` +Or in Gradle +```kotlin +implementation("com.github.gestalt-config:gestalt-validator-hibernate:${version}") +``` + +Then when building gestalt, you need to register the module config `HibernateModuleConfig` using the `HibernateModuleBuilder`. + +```java +ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); +Validator validator = factory.getValidator(); + +Gestalt gestalt = new GestaltBuilder() + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .setValidationEnabled(true) + .addModuleConfig(HibernateModuleBuilder.builder() + .setValidator(validator) + .build()) + .build(); + +gestalt.loadConfigs(); +``` + +For details on how to use the [Hibernate Validator](https://hibernate.org/validator/) see their documentation. + ## Guice dependency injection. Allow Gestalt to inject configuration directly into your classes using Guice using the `@InjectConfig` annotation on any class fields. This does not support constructor injection (due to Guice limitation) To enable add the `new GestaltModule(gestalt)` to your Guice Modules, then pass in your instance of Gestalt. diff --git a/build.gradle.kts b/build.gradle.kts index 50608c5fb..6b9067515 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -8,7 +8,7 @@ plugins { allprojects { group = "com.github.gestalt-config" - version = "0.26.0" + version = "0.27.0" } diff --git a/code-coverage-report/build.gradle.kts b/code-coverage-report/build.gradle.kts index ec02c1e14..8a1b16ed2 100644 --- a/code-coverage-report/build.gradle.kts +++ b/code-coverage-report/build.gradle.kts @@ -22,10 +22,13 @@ dependencies { jacocoAggregation(project(":gestalt-kodein-di")) jacocoAggregation(project(":gestalt-koin-di")) jacocoAggregation(project(":gestalt-kotlin")) + jacocoAggregation(project(":gestalt-micrometer")) jacocoAggregation(project(":gestalt-toml")) + jacocoAggregation(project(":gestalt-validator-hibernate")) jacocoAggregation(project(":gestalt-vault")) jacocoAggregation(project(":gestalt-yaml")) + // include additional tests. jacocoAggregation(project(":gestalt-test")) jacocoAggregation(project(":gestalt-examples:gestalt-sample")) diff --git a/gestalt-benchmark/src/jmh/java/org/github/gestalt/config/benchmark/Benchmarks.java b/gestalt-benchmark/src/jmh/java/org/github/gestalt/config/benchmark/Benchmarks.java index bb43e1150..40be997d5 100644 --- a/gestalt-benchmark/src/jmh/java/org/github/gestalt/config/benchmark/Benchmarks.java +++ b/gestalt-benchmark/src/jmh/java/org/github/gestalt/config/benchmark/Benchmarks.java @@ -36,13 +36,11 @@ public HttpPool GestaltConfig_Object(BenchmarkState state) throws GestaltExcepti } @Benchmark - @OutputTimeUnit(TimeUnit.SECONDS) public String GestaltConfig_String_No_Cache(BenchmarkState state) throws GestaltException { return state.gestaltNoCache.getConfig("http.pool.maxTotal", String.class); } @Benchmark - @OutputTimeUnit(TimeUnit.SECONDS) public HttpPool GestaltConfig_Object_No_Cache(BenchmarkState state) throws GestaltException { return state.gestaltNoCache.getConfig("http.pool", HttpPool.class); } diff --git a/gestalt-core/src/main/java/module-info.java b/gestalt-core/src/main/java/module-info.java index c4db6fa1d..4eeea66fb 100644 --- a/gestalt-core/src/main/java/module-info.java +++ b/gestalt-core/src/main/java/module-info.java @@ -7,6 +7,8 @@ uses org.github.gestalt.config.path.mapper.PathMapper; uses org.github.gestalt.config.post.process.PostProcessor; uses org.github.gestalt.config.post.process.transform.Transformer; + uses org.github.gestalt.config.validation.ConfigValidator; + uses org.github.gestalt.config.metrics.MetricsRecorder; exports org.github.gestalt.config; exports org.github.gestalt.config.annotations; @@ -30,6 +32,7 @@ exports org.github.gestalt.config.tag; exports org.github.gestalt.config.token; exports org.github.gestalt.config.utils; + exports org.github.gestalt.config.validation; provides org.github.gestalt.config.decoder.Decoder with org.github.gestalt.config.decoder.ArrayDecoder, diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/GestaltCache.java b/gestalt-core/src/main/java/org/github/gestalt/config/GestaltCache.java index 1955f9fe6..23027b327 100644 --- a/gestalt-core/src/main/java/org/github/gestalt/config/GestaltCache.java +++ b/gestalt-core/src/main/java/org/github/gestalt/config/GestaltCache.java @@ -31,6 +31,8 @@ public class GestaltCache implements Gestalt, CoreReloadListener { * * @param delegate real Gestalt to call for configs to cache. * @param defaultTags Default set of tags to apply to all calls to get a configuration where tags are not provided. + * @param metricsManager Metrics manager for submitting metrics + * @param gestaltConfig Gestalt Configuration */ public GestaltCache(Gestalt delegate, Tags defaultTags, MetricsManager metricsManager, GestaltConfig gestaltConfig) { this.delegate = delegate; diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/GestaltCore.java b/gestalt-core/src/main/java/org/github/gestalt/config/GestaltCore.java index 50440cf50..9c1dbe9cd 100644 --- a/gestalt-core/src/main/java/org/github/gestalt/config/GestaltCore.java +++ b/gestalt-core/src/main/java/org/github/gestalt/config/GestaltCore.java @@ -24,12 +24,14 @@ import org.github.gestalt.config.secret.rules.SecretConcealer; import org.github.gestalt.config.source.ConfigSource; import org.github.gestalt.config.source.ConfigSourcePackage; +import org.github.gestalt.config.tag.Tag; import org.github.gestalt.config.tag.Tags; import org.github.gestalt.config.token.Token; import org.github.gestalt.config.utils.ClassUtils; import org.github.gestalt.config.utils.ErrorsUtil; import org.github.gestalt.config.utils.GResultOf; import org.github.gestalt.config.utils.Pair; +import org.github.gestalt.config.validation.ValidationManager; import java.util.*; @@ -61,6 +63,8 @@ public class GestaltCore implements Gestalt, ConfigReloadListener { private final MetricsManager metricsManager; + private final ValidationManager validationManager; + private final DecoderContext decoderContext; /** @@ -77,13 +81,14 @@ public class GestaltCore implements Gestalt, ConfigReloadListener { * @param postProcessor postProcessor list of post processors * @param secretConcealer Utility for concealing secrets * @param metricsManager Manages reporting of metrics + * @param validationManager Validation Manager, for validating configuration objects * @param defaultTags Default set of tags to apply to all calls to get a configuration where tags are not provided. */ public GestaltCore(ConfigLoaderService configLoaderService, List configSourcePackages, DecoderService decoderService, SentenceLexer sentenceLexer, GestaltConfig gestaltConfig, ConfigNodeService configNodeService, CoreReloadListenersContainer reloadStrategy, List postProcessor, SecretConcealer secretConcealer, - MetricsManager metricsManager, Tags defaultTags) { + MetricsManager metricsManager, ValidationManager validationManager, Tags defaultTags) { this.configLoaderService = configLoaderService; this.sourcePackages = configSourcePackages; this.decoderService = decoderService; @@ -94,6 +99,7 @@ public GestaltCore(ConfigLoaderService configLoaderService, List T getConfig(String path, TypeCapture klass, Tags tags) throws Gest // most likely an optional.empty() Pair isOptionalAndDefault = ClassUtils.isOptionalAndDefault(klass.getRawType()); - return getConfigInternal(path, !isOptionalAndDefault.getFirst(), isOptionalAndDefault.getSecond(), klass, tags); + return getConfigurationInternal(path, !isOptionalAndDefault.getFirst(), isOptionalAndDefault.getSecond(), klass, tags); } @Override @@ -323,7 +329,7 @@ public T getConfig(String path, T defaultVal, TypeCapture klass, Tags tag Objects.requireNonNull(tags); try { - return getConfigInternal(path, false, defaultVal, klass, tags); + return getConfigurationInternal(path, false, defaultVal, klass, tags); } catch (GestaltException e) { logger.log(WARNING, e.getMessage()); } @@ -359,7 +365,7 @@ public Optional getConfigOptional(String path, TypeCapture klass, Tags Objects.requireNonNull(tags); try { - var results = getConfigInternal(path, false, null, klass, tags); + var results = getConfigurationInternal(path, false, null, klass, tags); return Optional.ofNullable(results); } catch (GestaltException e) { logger.log(WARNING, e.getMessage()); @@ -368,13 +374,15 @@ public Optional getConfigOptional(String path, TypeCapture klass, Tags return Optional.empty(); } - private T getConfigInternal(String path, boolean failOnErrors, T defaultVal, TypeCapture klass, Tags tags) + private T getConfigurationInternal(String path, boolean failOnErrors, T defaultVal, TypeCapture klass, Tags tags) throws GestaltException { Objects.requireNonNull(path); Objects.requireNonNull(klass); Objects.requireNonNull(tags); MetricsMarker getConfigMarker = null; + boolean defaultReturned = false; + Exception exceptionThrown = null; try { if (gestaltConfig.isMetricsEnabled() && metricsManager != null) { getConfigMarker = metricsManager.startGetConfig(path, klass, tags, failOnErrors); @@ -385,7 +393,7 @@ private T getConfigInternal(String path, boolean failOnErrors, T defaultVal, if (tokens.hasErrors()) { throw new GestaltException("Unable to parse path: " + combinedPath, tokens.getErrors()); } else { - GResultOf results = getConfigInternal(combinedPath, tokens.results(), klass, tags); + GResultOf results = getAndDecodeConfig2(combinedPath, tokens.results(), klass, tags); getConfigMetrics(results); @@ -399,9 +407,7 @@ private T getConfigInternal(String path, boolean failOnErrors, T defaultVal, ", for class: " + klass.getName() + " returning empty Optional", results.getErrors()); logger.log(gestaltConfig.getLogLevelForMissingValuesWhenDefaultOrOptional(), errorMsg); } - if (gestaltConfig.isMetricsEnabled() && metricsManager != null) { - metricsManager.finalizeMetric(getConfigMarker, Tags.of("default", "true")); - } + defaultReturned = true; return defaultVal; } @@ -413,11 +419,35 @@ private T getConfigInternal(String path, boolean failOnErrors, T defaultVal, } if (results.hasResults()) { - if (gestaltConfig.isMetricsEnabled() && metricsManager != null) { - metricsManager.finalizeMetric(getConfigMarker, Tags.of()); + var resultConfig = results.results(); + + // if we have a result, lets check if validation is enabled and if we should validate the object, + // then validate the result. + if (gestaltConfig.isValidationEnabled() && shouldValidate(klass)) { + var validationResults = validationManager.validator(resultConfig, path, klass, tags); + // if there are validation errors we can either fail with an exception or return the default value. + if (validationResults.hasErrors()) { + updateValidationMetrics(validationResults); + + if (failOnErrors) { + throw new GestaltException("Validation failed for config path: " + combinedPath + + ", and class: " + klass.getName(), validationResults.getErrors()); + + } else { + if (logger.isLoggable(WARNING)) { + String errorMsg = ErrorsUtil.buildErrorMessage("Validation failed for config path: " + + combinedPath + ", and class: " + klass.getName() + " returning default value", + validationResults.getErrors()); + logger.log(WARNING, errorMsg); + } + defaultReturned = true; + + return defaultVal; + } + } } - return results.results(); + return resultConfig; } } @@ -430,22 +460,33 @@ private T getConfigInternal(String path, boolean failOnErrors, T defaultVal, if (failOnErrors) { throw new GestaltException("No results for config path: " + combinedPath + ", and class: " + klass.getName()); } else { - if (gestaltConfig.isMetricsEnabled() && metricsManager != null) { - metricsManager.finalizeMetric(getConfigMarker, Tags.of("default", "true")); - } + defaultReturned = true; return defaultVal; } } catch (Exception ex) { - if (gestaltConfig.isMetricsEnabled() && metricsManager != null) { - metricsManager.finalizeMetric(getConfigMarker, Tags.of("exception", ex.getClass().getCanonicalName())); + exceptionThrown = ex; + throw ex; + } finally { + finalizeMetrics(getConfigMarker, defaultReturned, exceptionThrown); + } + } + + private void finalizeMetrics(MetricsMarker getConfigMarker, boolean defaultReturned, Exception exceptionThrown) { + if (gestaltConfig.isMetricsEnabled() && metricsManager != null && getConfigMarker != null) { + Set tagSet = new HashSet<>(); + if (defaultReturned) { + tagSet.add(Tag.of("default", "true")); } - throw ex; + if (exceptionThrown != null) { + tagSet.add(Tag.of("exception", exceptionThrown.getClass().getCanonicalName())); + } + metricsManager.finalizeMetric(getConfigMarker, Tags.of(tagSet)); } } - private GResultOf getConfigInternal(String path, List tokens, TypeCapture klass, Tags tags) { + private GResultOf getAndDecodeConfig2(String path, List tokens, TypeCapture klass, Tags tags) { GResultOf node = configNodeService.navigateToNode(path, tokens, tags); if (!node.hasErrors() || node.hasErrors(ValidationLevel.MISSING_VALUE)) { @@ -507,6 +548,15 @@ private void getConfigMetrics(GResultOf results) throws GestaltException } } + private void updateValidationMetrics(GResultOf results) { + if (gestaltConfig.isMetricsEnabled() && metricsManager != null) { + int validationErrors = results.getErrors().size(); + if (validationErrors != 0) { + metricsManager.recordMetric("get.config.validation.error", validationErrors, Tags.of()); + } + } + } + private boolean ignoreError(ValidationError error) { if (error.level().equals(ValidationLevel.WARN) && gestaltConfig.isTreatWarningsAsErrors()) { return false; @@ -522,6 +572,10 @@ private boolean ignoreError(ValidationError error) { return error.level() == ValidationLevel.WARN || error.level() == ValidationLevel.DEBUG; } + private boolean shouldValidate(TypeCapture klass) { + return !klass.isAssignableFrom(String.class) && !ClassUtils.isPrimitiveOrWrapper(klass.getRawType()); + } + /** * Prints out the contents of a config root for the tag. * diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/builder/GestaltBuilder.java b/gestalt-core/src/main/java/org/github/gestalt/config/builder/GestaltBuilder.java index 6b4853d2a..0b5dd8c50 100644 --- a/gestalt-core/src/main/java/org/github/gestalt/config/builder/GestaltBuilder.java +++ b/gestalt-core/src/main/java/org/github/gestalt/config/builder/GestaltBuilder.java @@ -30,6 +30,8 @@ import org.github.gestalt.config.source.ConfigSourcePackage; import org.github.gestalt.config.tag.Tags; import org.github.gestalt.config.utils.CollectionUtils; +import org.github.gestalt.config.validation.ConfigValidator; +import org.github.gestalt.config.validation.ValidationManager; import java.lang.System.Logger.Level; import java.time.format.DateTimeFormatter; @@ -69,6 +71,7 @@ public class GestaltBuilder { private SentenceLexer sentenceLexer = new PathLexer(); private GestaltConfig gestaltConfig = new GestaltConfig(); private MetricsManager metricsManager; + private ValidationManager validationManager; private ConfigNodeService configNodeService = new ConfigNodeManager(); private List configSourcePackages = new ArrayList<>(); private List> decoders = new ArrayList<>(); @@ -76,6 +79,7 @@ public class GestaltBuilder { private List postProcessors = new ArrayList<>(); private List pathMappers = new ArrayList<>(); private List metricsRecorders = new ArrayList<>(); + private List configValidators = new ArrayList<>(); private boolean useCacheDecorator = true; private Set securityRules = new HashSet<>( List.of("bearer", "cookie", "credential", "id", @@ -116,8 +120,12 @@ public class GestaltBuilder { // Default set of tags to apply to all calls to get a configuration where tags are not provided. private Tags defaultTags = Tags.of(); + // If we should enable metrics private Boolean metricsEnabled = null; + // If we should enable Validation + private Boolean validationEnabled = null; + /** * Adds all default decoders to the builder. Uses the ServiceLoader to find all registered Decoders and adds them * @@ -187,6 +195,20 @@ public GestaltBuilder addDefaultMetricsRecorder() { return this; } + /** + * Add default Validator to the builder. Uses the ServiceLoader to find all registered Validator and adds them + * + * @return GestaltBuilder builder + */ + public GestaltBuilder addDefaultValidators() { + List validatorsSet = new ArrayList<>(); + ServiceLoader loader = ServiceLoader.load(ConfigValidator.class); + loader.forEach(validatorsSet::add); + + configValidators.addAll(validatorsSet); + return this; + } + private void configurePostProcessors() { postProcessors.forEach(it -> { PostProcessorConfig config = new PostProcessorConfig(gestaltConfig, configNodeService, sentenceLexer, secretConcealer); @@ -215,6 +237,10 @@ private void configureMetricsRecorders() { metricsRecorders.forEach(it -> it.applyConfig(gestaltConfig)); } + private void configureValidators() { + configValidators.forEach(it -> it.applyConfig(gestaltConfig)); + } + /** * Add a single source to the builder. * @@ -519,11 +545,54 @@ public GestaltBuilder addMetricsRecorders(List metricsRecorders * @return GestaltBuilder builder */ public GestaltBuilder addMetricsRecorder(MetricsRecorder metricsRecorder) { - Objects.requireNonNull(metricsRecorder, "PathMapper should not be null"); + Objects.requireNonNull(metricsRecorder, "MetricsRecorder should not be null"); this.metricsRecorders.add(metricsRecorder); return this; } + /** + * Sets the list of Validator. Replaces any Validator already set. + * + * @param configValidators list of Validator to validate objects. + * @return GestaltBuilder builder + * @throws GestaltConfigurationException exception if there are no Validator + */ + public GestaltBuilder setValidators(List configValidators) throws GestaltConfigurationException { + if (configValidators == null || configValidators.isEmpty()) { + throw new GestaltConfigurationException("No Validators provided while setting"); + } + this.configValidators = configValidators; + + return this; + } + + /** + * List of Validator to add to the builder. + * + * @param validatorsSet list of Validator to add. + * @return GestaltBuilder builder + * @throws GestaltConfigurationException no Validator provided + */ + public GestaltBuilder addValidators(List validatorsSet) throws GestaltConfigurationException { + Objects.requireNonNull(validatorsSet, "Validator should not be null"); + + configValidators.addAll(validatorsSet); + + return this; + } + + /** + * Add a single Validator to the builder. + * + * @param configValidator add a single Validator + * @return GestaltBuilder builder + */ + public GestaltBuilder addValidator(ConfigValidator configValidator) { + Objects.requireNonNull(configValidator, "Validator should not be null"); + this.configValidators.add(configValidator); + return this; + } + /** * Set the mask to use when replacing a leaf value matching a security rule. * @@ -612,6 +681,24 @@ public GestaltBuilder setMetricsManager(MetricsManager metricsManager) { return this; } + /** + * Sets the ValidationManager if you want to provide your own. Otherwise, a default is provided. + * + *

If there are any Validators, it will not add the default Validators. + * So you will need to add the defaults manually if needed. + * + * @param validationManager Validation Manager + * @return GestaltBuilder builder + */ + public GestaltBuilder setValidationManager(ValidationManager validationManager) { + Objects.requireNonNull(validationManager, "ValidationManager should not be null"); + this.validationManager = validationManager; + validationManager.addValidators(configValidators); + return this; + } + + + /** * Set the decoder service if you want to provide your own. Otherwise a default is provided. * If there are any decoders set, it will not add the default decoders. So you will need to add the defaults manually if needed. @@ -794,6 +881,17 @@ public GestaltBuilder setMetricsEnabled(Boolean metricsEnabled) { return this; } + /** + * If we are to enable validation. + * + * @param validationEnabled If we are to enable validation + * @return GestaltBuilder builder + */ + public GestaltBuilder setValidationEnabled(Boolean validationEnabled) { + this.validationEnabled = validationEnabled; + return this; + } + /** * Set a date decoder format. Used to decode date times. * @@ -1022,18 +1120,28 @@ public Gestalt build() throws GestaltConfigurationException { logger.log(TRACE, "No decoders provided, using defaults"); addDefaultDecoders(); } + decoders = decoders.stream().filter(Objects::nonNull).collect(Collectors.toList()); // setup the default path mappers, if there are none, add the default ones. if (pathMappers.isEmpty()) { logger.log(TRACE, "No path mapper provided, using defaults"); addDefaultPathMappers(); } + pathMappers = pathMappers.stream().filter(Objects::nonNull).collect(Collectors.toList()); // setup the default metricsRecorders, if there are none add the default ones. if (metricsRecorders.isEmpty()) { logger.log(TRACE, "No metric recorders provided, using defaults"); addDefaultMetricsRecorder(); } + metricsRecorders = metricsRecorders.stream().filter(Objects::nonNull).collect(Collectors.toList()); + + // setup the default validators, if there are none add the default ones. + if (configValidators.isEmpty()) { + logger.log(TRACE, "No validators recorders provided, using defaults"); + addDefaultValidators(); + } + configValidators = configValidators.stream().filter(Objects::nonNull).collect(Collectors.toList()); // if the metricsManager does not exist, create it. @@ -1044,6 +1152,14 @@ public Gestalt build() throws GestaltConfigurationException { metricsManager.addMetricsRecorders(metricsRecorders); } + // if the metricsManager does not exist, create it. + // Otherwise, get all the recorders from the metricsManager, combine them with the ones in the builder, + if (validationManager == null) { + validationManager = new ValidationManager(configValidators); + } else { + validationManager.addValidators(configValidators); + } + // if the decoderService does not exist, create it. // Otherwise get all the decoders from the decoderService, combine them with the ones in the builder, // and update the decoderService @@ -1060,11 +1176,13 @@ public Gestalt build() throws GestaltConfigurationException { logger.log(TRACE, "No decoders provided, using defaults"); addDefaultConfigLoaders(); } + configLoaders = configLoaders.stream().filter(Objects::nonNull).collect(Collectors.toList()); if (postProcessors.isEmpty()) { logger.log(TRACE, "No post processors provided, using defaults"); addDefaultPostProcessors(); } + postProcessors = postProcessors.stream().filter(Objects::nonNull).collect(Collectors.toList()); // get all the config loaders from the configLoaderRegistry, combine them with the ones in the builder, // and update the configLoaderRegistry @@ -1078,11 +1196,13 @@ public Gestalt build() throws GestaltConfigurationException { configurePostProcessors(); configurePathMappers(); configureMetricsRecorders(); + configureValidators(); // create a new GestaltCoreReloadStrategy to listen for Gestalt Core Reloads. CoreReloadListenersContainer coreReloadListenersContainer = new CoreReloadListenersContainer(); final GestaltCore gestaltCore = new GestaltCore(configLoaderService, configSourcePackages, decoderService, sentenceLexer, - gestaltConfig, configNodeService, coreReloadListenersContainer, postProcessors, secretConcealer, metricsManager, defaultTags); + gestaltConfig, configNodeService, coreReloadListenersContainer, postProcessors, secretConcealer, metricsManager, + validationManager, defaultTags); // register gestaltCore with all the source reload strategies. reloadStrategies.forEach(it -> it.registerListener(gestaltCore)); @@ -1152,6 +1272,9 @@ private GestaltConfig rebuildConfig() { newConfig.setMetricsEnabled(Objects.requireNonNullElseGet(metricsEnabled, () -> gestaltConfig.isMetricsEnabled())); + newConfig.setValidationEnabled(Objects.requireNonNullElseGet(validationEnabled, + () -> gestaltConfig.isValidationEnabled())); + return newConfig; } } diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/entity/GestaltConfig.java b/gestalt-core/src/main/java/org/github/gestalt/config/entity/GestaltConfig.java index 2d51aa238..560b0fc64 100644 --- a/gestalt-core/src/main/java/org/github/gestalt/config/entity/GestaltConfig.java +++ b/gestalt-core/src/main/java/org/github/gestalt/config/entity/GestaltConfig.java @@ -48,6 +48,9 @@ public class GestaltConfig { // if metrics should be enabled private boolean metricsEnabled = false; + // if validation should be enabled. + private boolean validationEnabled = false; + /** * Treat all warnings as errors. * @@ -333,6 +336,24 @@ public void setMetricsEnabled(boolean metricsEnabled) { this.metricsEnabled = metricsEnabled; } + /** + * Get if validation is enabled. + * + * @return if validation is enabled + */ + public boolean isValidationEnabled() { + return validationEnabled; + } + + /** + * Set if validation is enabled. + * + * @param validationEnabled if validation is enabled. + */ + public void setValidationEnabled(boolean validationEnabled) { + this.validationEnabled = validationEnabled; + } + /** * Register an external module configuration. * diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/metrics/MetricsManager.java b/gestalt-core/src/main/java/org/github/gestalt/config/metrics/MetricsManager.java index 99141663e..bee20b970 100644 --- a/gestalt-core/src/main/java/org/github/gestalt/config/metrics/MetricsManager.java +++ b/gestalt-core/src/main/java/org/github/gestalt/config/metrics/MetricsManager.java @@ -19,7 +19,9 @@ public final class MetricsManager { private final Map metricsRecorders; public MetricsManager(List recorder) { - this.metricsRecorders = recorder.stream().collect(Collectors.toMap(MetricsRecorder::recorderId, Function.identity())); + this.metricsRecorders = recorder + .stream() + .collect(Collectors.toMap(MetricsRecorder::recorderId, Function.identity())); } public void addMetricsRecorder(MetricsRecorder recorder) { diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/validation/ConfigValidator.java b/gestalt-core/src/main/java/org/github/gestalt/config/validation/ConfigValidator.java new file mode 100644 index 000000000..7a7544205 --- /dev/null +++ b/gestalt-core/src/main/java/org/github/gestalt/config/validation/ConfigValidator.java @@ -0,0 +1,34 @@ +package org.github.gestalt.config.validation; + +import org.github.gestalt.config.entity.GestaltConfig; +import org.github.gestalt.config.reflect.TypeCapture; +import org.github.gestalt.config.tag.Tags; +import org.github.gestalt.config.utils.GResultOf; + +/** + * Interface for validating objects. + * + * @author Colin Redmond (c) 2024. + */ +public interface ConfigValidator { + + /** + * If your Validator needs access to the Gestalt Config. + * + * @param config Gestalt configuration + */ + default void applyConfig(GestaltConfig config) {} + + /** + * Returns the {@link GResultOf} with the validation results. If the object is ok it will return the result with no errors. + * If there are validation errors they will be returned. + * + * @param obj object to validate. + * @param path path the object was located at + * @param klass the type of object. + * @param tags any tags used to retrieve te object + * @return The validation results with either errors or a successful obj. + * @param Class of the object. + */ + GResultOf validator(T obj, String path, TypeCapture klass, Tags tags); +} diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/validation/ValidationManager.java b/gestalt-core/src/main/java/org/github/gestalt/config/validation/ValidationManager.java new file mode 100644 index 000000000..868c36558 --- /dev/null +++ b/gestalt-core/src/main/java/org/github/gestalt/config/validation/ValidationManager.java @@ -0,0 +1,55 @@ +package org.github.gestalt.config.validation; + +import org.github.gestalt.config.reflect.TypeCapture; +import org.github.gestalt.config.tag.Tags; +import org.github.gestalt.config.utils.GResultOf; + +import java.util.List; +import java.util.stream.Collectors; + +/** + * Manages all validations. Gestalt calls the manager who then forwards the calls to all registered validators. + * It collates the results and returns either the object or the errors. + * + * @author Colin Redmond (c) 2024. + */ +public final class ValidationManager { + + private final List configValidators; + + public ValidationManager(List configValidators) { + this.configValidators = configValidators; + } + + /** + * Add a list of validators to the manager. + * + * @param validatorsSet list of validators + */ + public void addValidators(List validatorsSet) { + this.configValidators.addAll(validatorsSet); + } + + /** + * Calls all the validators, and it collates the results and returns either the object or the errors. + * + * @param obj object to validate. + * @param path path the object was located at + * @param klass the type of object. + * @param tags any tags used to retrieve te object + * @param Class of the object. + * @return The validation results with either errors or a successful obj. + */ + public GResultOf validator(T obj, String path, TypeCapture klass, Tags tags) { + var errors = configValidators.stream() + .map(it -> it.validator(obj, path, klass, tags)) + .flatMap(it -> it.getErrors().stream()) + .collect(Collectors.toList()); + + if (errors.isEmpty()) { + return GResultOf.result(obj); + } else { + return GResultOf.errors(errors); + } + } +} diff --git a/gestalt-core/src/test/java/org/github/gestalt/config/GestaltConfigValidatorTest.java b/gestalt-core/src/test/java/org/github/gestalt/config/GestaltConfigValidatorTest.java new file mode 100644 index 000000000..84f46125b --- /dev/null +++ b/gestalt-core/src/test/java/org/github/gestalt/config/GestaltConfigValidatorTest.java @@ -0,0 +1,262 @@ +package org.github.gestalt.config; + +import org.github.gestalt.config.builder.GestaltBuilder; +import org.github.gestalt.config.exceptions.GestaltException; +import org.github.gestalt.config.source.MapConfigSourceBuilder; +import org.github.gestalt.config.tag.Tags; +import org.github.gestalt.config.test.classes.DBInfo; +import org.github.gestalt.config.validation.TestConfigValidator; +import org.github.gestalt.config.validation.ConfigValidator; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; +import java.util.logging.LogManager; + +public class GestaltConfigValidatorTest { + + @BeforeAll + public static void beforeAll() { + try (InputStream is = GestaltMetricsTest.class.getClassLoader().getResourceAsStream("logging.properties")) { + LogManager.getLogManager().readConfiguration(is); + } catch (IOException e) { + // dont care + } + } + + @Test + public void testValidatorGetOkNoValidator() throws GestaltException { + + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "123"); + configs.put("db.uri", "my.sql.com"); + + Map configs2 = new HashMap<>(); + configs2.put("db.password", "test2"); + configs2.put("db.port", "456"); + configs2.put("db.uri", "my.postgresql.com"); + + Gestalt gestalt = new GestaltBuilder() + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs2).setTags(Tags.environment("dev")).build()) + .setValidationEnabled(true) + .build(); + + gestalt.loadConfigs(); + + Assertions.assertEquals(456, gestalt.getConfig("db.port", Integer.class, Tags.environment("dev"))); + } + + @Test + public void testValidatorGetOkNullValidator() throws GestaltException { + + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "123"); + configs.put("db.uri", "my.sql.com"); + + Map configs2 = new HashMap<>(); + configs2.put("db.password", "test2"); + configs2.put("db.port", "456"); + configs2.put("db.uri", "my.postgresql.com"); + + var validatorArray = new ArrayList(); + validatorArray.add(null); + + Gestalt gestalt = new GestaltBuilder() + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs2).setTags(Tags.environment("dev")).build()) + .setValidationEnabled(true) + .addValidators(validatorArray) + .build(); + + gestalt.loadConfigs(); + + Assertions.assertEquals(456, gestalt.getConfig("db.port", Integer.class, Tags.environment("dev"))); + } + + @Test + public void testValidatorGetOkPrimitive() throws GestaltException { + + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "123"); + configs.put("db.uri", "my.sql.com"); + + Map configs2 = new HashMap<>(); + configs2.put("db.password", "test2"); + configs2.put("db.port", "456"); + configs2.put("db.uri", "my.postgresql.com"); + + Gestalt gestalt = new GestaltBuilder() + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs2).setTags(Tags.environment("dev")).build()) + .setValidationEnabled(true) + .addValidator(new TestConfigValidator(true)) + .build(); + + gestalt.loadConfigs(); + + Assertions.assertEquals(456, gestalt.getConfig("db.port", Integer.class, Tags.environment("dev"))); + } + + @Test + public void testValidatorGetOkString() throws GestaltException { + + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "123"); + configs.put("db.uri", "my.sql.com"); + + Map configs2 = new HashMap<>(); + configs2.put("db.password", "test2"); + configs2.put("db.port", "456"); + configs2.put("db.uri", "my.postgresql.com"); + + Gestalt gestalt = new GestaltBuilder() + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs2).setTags(Tags.environment("dev")).build()) + .setValidationEnabled(true) + .addValidator(new TestConfigValidator(true)) + .build(); + + gestalt.loadConfigs(); + + Assertions.assertEquals("test2", gestalt.getConfig("db.password", String.class, Tags.environment("dev"))); + } + + @Test + public void testValidatorGetWouldFailPrimitive() throws GestaltException { + + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "123"); + configs.put("db.uri", "my.sql.com"); + + Map configs2 = new HashMap<>(); + configs2.put("db.password", "test2"); + configs2.put("db.port", "456"); + configs2.put("db.uri", "my.postgresql.com"); + + Gestalt gestalt = new GestaltBuilder() + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs2).setTags(Tags.environment("dev")).build()) + .setValidationEnabled(true) + .addValidator(new TestConfigValidator(false)) + .build(); + + gestalt.loadConfigs(); + + Assertions.assertEquals(456, gestalt.getConfig("db.port", Integer.class, Tags.environment("dev"))); + } + + @Test + public void testValidatorGetWouldFailString() throws GestaltException { + + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "123"); + configs.put("db.uri", "my.sql.com"); + + Map configs2 = new HashMap<>(); + configs2.put("db.password", "test2"); + configs2.put("db.port", "456"); + configs2.put("db.uri", "my.postgresql.com"); + + Gestalt gestalt = new GestaltBuilder() + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs2).setTags(Tags.environment("dev")).build()) + .setValidationEnabled(true) + .addValidator(new TestConfigValidator(false)) + .build(); + + gestalt.loadConfigs(); + + Assertions.assertEquals("test2", gestalt.getConfig("db.password", String.class, Tags.environment("dev"))); + } + + @Test + public void testValidatorGetOkClass() throws GestaltException { + + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "123"); + configs.put("db.uri", "my.sql.com"); + + Map configs2 = new HashMap<>(); + configs2.put("db.password", "test2"); + configs2.put("db.port", "456"); + configs2.put("db.uri", "my.postgresql.com"); + + Gestalt gestalt = new GestaltBuilder() + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs2).setTags(Tags.environment("dev")).build()) + .setValidationEnabled(true) + .addValidator(new TestConfigValidator(true)) + .build(); + + gestalt.loadConfigs(); + + Assertions.assertEquals("test2", gestalt.getConfig("db", DBInfo.class, Tags.environment("dev")).getPassword()); + } + + @Test + public void testValidatorGetFailClass() throws GestaltException { + + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "123"); + configs.put("db.uri", "my.sql.com"); + + Map configs2 = new HashMap<>(); + configs2.put("db.password", "test2"); + configs2.put("db.port", "456"); + configs2.put("db.uri", "my.postgresql.com"); + + Gestalt gestalt = new GestaltBuilder() + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs2).setTags(Tags.environment("dev")).build()) + .setValidationEnabled(true) + .addValidator(new TestConfigValidator(false)) + .build(); + + gestalt.loadConfigs(); + + var ex = Assertions.assertThrows(GestaltException.class, + () -> gestalt.getConfig("db", DBInfo.class, Tags.environment("dev"))); + + Assertions.assertEquals("Validation failed for config path: db, and " + + "class: org.github.gestalt.config.test.classes.DBInfo\n" + + " - level: ERROR, message: something broke", ex.getMessage()); + } + + @Test + public void testValidatorGetFailClassReturnDefault() throws GestaltException { + + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "123"); + configs.put("db.uri", "my.sql.com"); + + Map configs2 = new HashMap<>(); + configs2.put("db.password", "test2"); + configs2.put("db.port", "456"); + configs2.put("db.uri", "my.postgresql.com"); + + Gestalt gestalt = new GestaltBuilder() + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs2).setTags(Tags.environment("dev")).build()) + .setValidationEnabled(true) + .addValidator(new TestConfigValidator(false)) + .build(); + + gestalt.loadConfigs(); + Assertions.assertTrue(gestalt.getConfigOptional("db", DBInfo.class, Tags.environment("dev")).isEmpty()); + } +} diff --git a/gestalt-core/src/test/java/org/github/gestalt/config/GestaltMetricsTest.java b/gestalt-core/src/test/java/org/github/gestalt/config/GestaltMetricsTest.java index d6de48018..a342cc6a8 100644 --- a/gestalt-core/src/test/java/org/github/gestalt/config/GestaltMetricsTest.java +++ b/gestalt-core/src/test/java/org/github/gestalt/config/GestaltMetricsTest.java @@ -2,18 +2,21 @@ import org.github.gestalt.config.builder.GestaltBuilder; import org.github.gestalt.config.exceptions.GestaltException; +import org.github.gestalt.config.metrics.MetricsRecorder; import org.github.gestalt.config.metrics.TestMetricsRecorder; import org.github.gestalt.config.reload.ManualConfigReloadStrategy; import org.github.gestalt.config.source.MapConfigSourceBuilder; import org.github.gestalt.config.tag.Tags; import org.github.gestalt.config.test.classes.DBInfo; import org.github.gestalt.config.test.classes.DBInfoOptional; +import org.github.gestalt.config.validation.TestConfigValidator; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import java.io.IOException; import java.io.InputStream; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -61,6 +64,58 @@ public void testMetricsGetOk() throws GestaltException { Assertions.assertEquals(Tags.environment("dev"), metricsRecorder.metrics.get("db.password").tags); } + @Test + public void testMetricsGetOkNoMetricRecorder() throws GestaltException { + + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "123"); + configs.put("db.uri", "my.sql.com"); + + Map configs2 = new HashMap<>(); + configs2.put("db.password", "test2"); + configs2.put("db.port", "456"); + configs2.put("db.uri", "my.postgresql.com"); + + Gestalt gestalt = new GestaltBuilder() + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs2).setTags(Tags.environment("dev")).build()) + .setMetricsEnabled(true) + .build(); + + gestalt.loadConfigs(); + + Assertions.assertEquals("test2", gestalt.getConfig("db.password", String.class, Tags.environment("dev"))); + } + + @Test + public void testMetricsGetOkNullMetricRecorder() throws GestaltException { + + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "123"); + configs.put("db.uri", "my.sql.com"); + + Map configs2 = new HashMap<>(); + configs2.put("db.password", "test2"); + configs2.put("db.port", "456"); + configs2.put("db.uri", "my.postgresql.com"); + + var metricRecordersArray = new ArrayList(); + metricRecordersArray.add(null); + + Gestalt gestalt = new GestaltBuilder() + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs2).setTags(Tags.environment("dev")).build()) + .setMetricsEnabled(true) + .addMetricsRecorders(metricRecordersArray) + .build(); + + gestalt.loadConfigs(); + + Assertions.assertEquals("test2", gestalt.getConfig("db.password", String.class, Tags.environment("dev"))); + } + @Test public void testMetricsGetCache() throws GestaltException { @@ -307,6 +362,84 @@ public void testMetricsGetWarning() throws GestaltException { Assertions.assertEquals(Tags.of(), metricsRecorder.metrics.get("get.config.warning").tags); } + @Test + public void testMetricsGetFailValidation() throws GestaltException { + + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "123"); + configs.put("db.uri", "my.sql.com"); + + Map configs2 = new HashMap<>(); + configs2.put("db.password", "test2"); + configs2.put("db.port", "456"); + configs2.put("db.uri", "my.postgresql.com"); + + var metricsRecorder = new TestMetricsRecorder(0); + + Gestalt gestalt = new GestaltBuilder() + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs2).setTags(Tags.environment("dev")).build()) + .setMetricsRecorders(List.of(metricsRecorder)) + .setMetricsEnabled(true) + .setValidationEnabled(true) + .addValidator(new TestConfigValidator(false)) + .build(); + + gestalt.loadConfigs(); + + var ex = Assertions.assertThrows(GestaltException.class, + () -> gestalt.getConfig("db", DBInfo.class, Tags.environment("dev"))); + + + Assertions.assertEquals("Validation failed for config path: db, and class: " + + "org.github.gestalt.config.test.classes.DBInfo\n" + + " - level: ERROR, message: something broke", ex.getMessage()); + Assertions.assertEquals("db", metricsRecorder.metrics.get("db").path); + Assertions.assertEquals(10.0D, metricsRecorder.metrics.get("db").data); + Assertions.assertEquals(Tags.of("environment", "dev", "exception", "org.github.gestalt.config.exceptions.GestaltException"), + metricsRecorder.metrics.get("db").tags); + + Assertions.assertEquals(1.0, metricsRecorder.metrics.get("get.config.validation.error").data); + Assertions.assertEquals(Tags.of(), metricsRecorder.metrics.get("get.config.validation.error").tags); + } + + @Test + public void testMetricsGetFailValidationOptional() throws GestaltException { + + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "123"); + configs.put("db.uri", "my.sql.com"); + + Map configs2 = new HashMap<>(); + configs2.put("db.password", "test2"); + configs2.put("db.port", "456"); + configs2.put("db.uri", "my.postgresql.com"); + + var metricsRecorder = new TestMetricsRecorder(0); + + Gestalt gestalt = new GestaltBuilder() + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs2).setTags(Tags.environment("dev")).build()) + .setMetricsRecorders(List.of(metricsRecorder)) + .setMetricsEnabled(true) + .setValidationEnabled(true) + .addValidator(new TestConfigValidator(false)) + .build(); + + gestalt.loadConfigs(); + + Assertions.assertTrue(gestalt.getConfigOptional("db", DBInfo.class, Tags.environment("dev")).isEmpty()); + + Assertions.assertEquals("db", metricsRecorder.metrics.get("db").path); + Assertions.assertEquals(10.0D, metricsRecorder.metrics.get("db").data); + Assertions.assertEquals(Tags.of("environment", "dev", "default", "true"), metricsRecorder.metrics.get("db").tags); + + Assertions.assertEquals(1.0, metricsRecorder.metrics.get("get.config.validation.error").data); + Assertions.assertEquals(Tags.of(), metricsRecorder.metrics.get("get.config.validation.error").tags); + } + @Test public void testMetricsReload() throws GestaltException { diff --git a/gestalt-core/src/test/java/org/github/gestalt/config/GestaltTest.java b/gestalt-core/src/test/java/org/github/gestalt/config/GestaltTest.java index c89e279a3..5dc6a4d09 100644 --- a/gestalt-core/src/test/java/org/github/gestalt/config/GestaltTest.java +++ b/gestalt-core/src/test/java/org/github/gestalt/config/GestaltTest.java @@ -516,7 +516,7 @@ public void testPostProcessorNoResults() throws GestaltException { new DecoderRegistry(List.of(new DoubleDecoder(), new LongDecoder(), new IntegerDecoder(), new StringDecoder()), configNodeManager, lexer, List.of(new StandardPathMapper())), lexer, new GestaltConfig(), configNodeManager, null, - Collections.singletonList(new TestPostProcessor("aaa")), secretConcealer, null, Tags.of()); + Collections.singletonList(new TestPostProcessor("aaa")), secretConcealer, null, null, Tags.of()); Mockito.when(configNodeManager.postProcess(Mockito.any())).thenReturn(GResultOf.resultOf(null, Collections.emptyList())); @@ -547,7 +547,7 @@ public void testPostProcessorError() throws GestaltException { new DecoderRegistry(List.of(new DoubleDecoder(), new LongDecoder(), new IntegerDecoder(), new StringDecoder()), configNodeManager, lexer, List.of(new StandardPathMapper())), lexer, new GestaltConfig(), configNodeManager, null, - Collections.singletonList(new TestPostProcessor("aaa")), secretConcealer, null, Tags.of()); + Collections.singletonList(new TestPostProcessor("aaa")), secretConcealer, null, null, Tags.of()); Mockito.when(configNodeManager.postProcess(Mockito.any())).thenReturn( GResultOf.resultOf(true, Collections.singletonList(new ValidationError.ArrayInvalidIndex(-1, "test")))); @@ -582,7 +582,7 @@ public void testPostProcessorMinorWarnings() throws GestaltException { new DecoderRegistry(List.of(new DoubleDecoder(), new LongDecoder(), new IntegerDecoder(), new StringDecoder()), configNodeManager, lexer, List.of(new StandardPathMapper())), lexer, config, configNodeManager, null, - Collections.singletonList(new TestPostProcessor("aaa")), secretConcealer, null, Tags.of()); + Collections.singletonList(new TestPostProcessor("aaa")), secretConcealer, null, null, Tags.of()); Mockito.when(configNodeManager.postProcess(Mockito.any())).thenReturn( GResultOf.resultOf(true, Collections.singletonList(new ValidationError.ArrayMissingIndex(1, "test")))); @@ -782,7 +782,8 @@ public void testMergeArraysMissingIndex() throws GestaltException { MapConfigSourceBuilder.builder().setCustomConfig(configs2).build()), new DecoderRegistry(List.of(new DoubleDecoder(), new LongDecoder(), new IntegerDecoder(), new StringDecoder(), new ListDecoder()), configNodeManager, lexer, List.of(new StandardPathMapper())), - lexer, config, new ConfigNodeManager(), null, Collections.emptyList(), secretConcealer, null, Tags.of()); + lexer, config, new ConfigNodeManager(), null, Collections.emptyList(), secretConcealer, null, null, + Tags.of()); gestalt.loadConfigs(); List errors = gestalt.getLoadErrors(); @@ -842,7 +843,8 @@ public void testNoSources() throws GestaltException { List.of(), new DecoderRegistry(List.of(new DoubleDecoder(), new LongDecoder(), new IntegerDecoder(), new StringDecoder()), configNodeManager, lexer, List.of(new StandardPathMapper())), - lexer, new GestaltConfig(), new ConfigNodeManager(), null, Collections.emptyList(), secretConcealer, null, Tags.of()); + lexer, new GestaltConfig(), new ConfigNodeManager(), null, Collections.emptyList(), secretConcealer, + null, null, Tags.of()); var ex = Assertions.assertThrows(GestaltException.class, () -> gestalt.loadConfigs()); assertThat(ex).isInstanceOf(GestaltException.class) @@ -862,7 +864,8 @@ public void testNullSources() throws GestaltException { null, new DecoderRegistry(List.of(new DoubleDecoder(), new LongDecoder(), new IntegerDecoder(), new StringDecoder()), configNodeManager, lexer, List.of(new StandardPathMapper())), - lexer, new GestaltConfig(), new ConfigNodeManager(), null, Collections.emptyList(), secretConcealer, null, Tags.of()); + lexer, new GestaltConfig(), new ConfigNodeManager(), null, Collections.emptyList(), secretConcealer, + null, null, Tags.of()); var ex = Assertions.assertThrows(GestaltException.class, () -> gestalt.loadConfigs()); assertThat(ex).isInstanceOf(GestaltException.class) @@ -889,7 +892,8 @@ public void testNoEncoder() throws GestaltException { List.of(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()), new DecoderRegistry(Collections.singletonList(new StringDecoder()), configNodeManager, lexer, List.of(new StandardPathMapper())), - lexer, new GestaltConfig(), new ConfigNodeManager(), null, Collections.emptyList(), secretConcealer, null, Tags.of()); + lexer, new GestaltConfig(), new ConfigNodeManager(), null, Collections.emptyList(), secretConcealer, + null, null, Tags.of()); gestalt.loadConfigs(); @@ -923,7 +927,8 @@ public void testMultipleEncoder() throws GestaltException { List.of(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()), new DecoderRegistry(List.of(new StringDecoder(), new ExceptionDecoder()), configNodeManager, lexer, List.of(new StandardPathMapper())), - lexer, new GestaltConfig(), new ConfigNodeManager(), null, Collections.emptyList(), secretConcealer, null, Tags.of()); + lexer, new GestaltConfig(), new ConfigNodeManager(), null, Collections.emptyList(), secretConcealer, + null, null, Tags.of()); gestalt.loadConfigs(); @@ -957,7 +962,8 @@ public void testLoadConfigError() throws GestaltException { List.of(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()), new DecoderRegistry(List.of(new DoubleDecoder(), new LongDecoder(), new IntegerDecoder(), new StringDecoder()), configNodeManager, lexer, List.of(new StandardPathMapper())), - lexer, new GestaltConfig(), configNodeManager, null, Collections.emptyList(), secretConcealer, null, Tags.of()); + lexer, new GestaltConfig(), configNodeManager, null, Collections.emptyList(), secretConcealer, + null, null, Tags.of()); var ex = Assertions.assertThrows(GestaltException.class, gestalt::loadConfigs); assertThat(ex).isInstanceOf(GestaltException.class) @@ -1023,7 +1029,8 @@ public void testNoResultsLoadingNode() throws GestaltException { List.of(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()), new DecoderRegistry(List.of(new DoubleDecoder(), new LongDecoder(), new IntegerDecoder(), new StringDecoder()), configNodeManager, lexer, List.of(new StandardPathMapper())), - lexer, config, new ConfigNodeManager(), null, Collections.emptyList(), secretConcealer, null, Tags.of()); + lexer, config, new ConfigNodeManager(), null, Collections.emptyList(), secretConcealer, + null, null, Tags.of()); GestaltConfigurationException e = Assertions.assertThrows(GestaltConfigurationException.class, gestalt::loadConfigs); Assertions.assertEquals("No results found for node", e.getMessage()); @@ -1237,7 +1244,7 @@ public void testReloadNullReloadSource() throws GestaltException { new DecoderRegistry(List.of(new DoubleDecoder(), new LongDecoder(), new IntegerDecoder(), new StringDecoder()), configNodeManager, lexer, List.of(new StandardPathMapper())), lexer, new GestaltConfig(), configNodeManager, coreReloadListenersContainer, Collections.emptyList(), secretConcealer, - null, Tags.of()); + null, null, Tags.of()); gestalt.loadConfigs(); List errors = gestalt.getLoadErrors(); @@ -1300,7 +1307,7 @@ public void testReloadNullSource() throws GestaltException { new DecoderRegistry(List.of(new DoubleDecoder(), new LongDecoder(), new IntegerDecoder(), new StringDecoder()), configNodeManager, lexer, List.of(new StandardPathMapper())), lexer, new GestaltConfig(), configNodeManager, coreReloadListenersContainer, Collections.emptyList(), secretConcealer, - null, Tags.of()); + null, null, Tags.of()); var ex = Assertions.assertThrows(GestaltException.class, () -> gestalt.reload(sourcePackage)); assertThat(ex).hasMessage("No sources provided, unable to reload any configs"); @@ -1335,7 +1342,7 @@ public void testReloadUnknownSource() throws GestaltException { new DecoderRegistry(List.of(new DoubleDecoder(), new LongDecoder(), new IntegerDecoder(), new StringDecoder()), configNodeManager, lexer, List.of(new StandardPathMapper())), lexer, new GestaltConfig(), configNodeManager, coreReloadListenersContainer, Collections.emptyList(), secretConcealer, - null, Tags.of()); + null, null, Tags.of()); gestalt.loadConfigs(); List errors = gestalt.getLoadErrors(); @@ -1408,7 +1415,7 @@ public void testReloadNoResults() throws GestaltException { new DecoderRegistry(List.of(new DoubleDecoder(), new LongDecoder(), new IntegerDecoder(), new StringDecoder()), configNodeManager, lexer, List.of(new StandardPathMapper())), lexer, new GestaltConfig(), configNodeManager, coreReloadListenersContainer, Collections.emptyList(), secretConcealer, - null, Tags.of()); + null, null, Tags.of()); gestalt.loadConfigs(); List errors = gestalt.getLoadErrors(); @@ -1473,7 +1480,7 @@ public void testReloadRemoveListener() throws GestaltException { new DecoderRegistry(List.of(new DoubleDecoder(), new LongDecoder(), new IntegerDecoder(), new StringDecoder()), configNodeManager, lexer, List.of(new StandardPathMapper())), lexer, new GestaltConfig(), configNodeManager, coreReloadListenersContainer, Collections.emptyList(), secretConcealer, - null, Tags.of()); + null, null, Tags.of()); gestalt.loadConfigs(); List errors = gestalt.getLoadErrors(); @@ -1601,7 +1608,7 @@ public void reloadListener() throws GestaltException { new OptionalDoubleDecoder(), new OptionalIntDecoder(), new OptionalLongDecoder()), configNodeManager, lexer, List.of(new StandardPathMapper())), lexer, new GestaltConfig(), configNodeManager, coreReloadListenersContainer, Collections.emptyList(), secretConcealer, - null, Tags.of("env", "dev")); + null, null, Tags.of("env", "dev")); gestalt.loadConfigs(); diff --git a/gestalt-core/src/test/java/org/github/gestalt/config/builder/GestaltBuilderTest.java b/gestalt-core/src/test/java/org/github/gestalt/config/builder/GestaltBuilderTest.java index 54681b27e..0b78f9bb0 100644 --- a/gestalt-core/src/test/java/org/github/gestalt/config/builder/GestaltBuilderTest.java +++ b/gestalt-core/src/test/java/org/github/gestalt/config/builder/GestaltBuilderTest.java @@ -35,6 +35,8 @@ import org.github.gestalt.config.test.classes.DBInfo; import org.github.gestalt.config.token.Token; import org.github.gestalt.config.utils.GResultOf; +import org.github.gestalt.config.validation.TestConfigValidator; +import org.github.gestalt.config.validation.ValidationManager; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -116,6 +118,10 @@ public void build() throws GestaltException { .addMetricsRecorders(List.of(new TestMetricsRecorder(1))) .setMetricsRecorders(List.of(new TestMetricsRecorder(0), new TestMetricsRecorder(1))) .setMetricsManager(new MetricsManager(List.of())) + .addValidator(new TestConfigValidator(true)) + .addValidators(List.of(new TestConfigValidator(true))) + .setValidators(List.of(new TestConfigValidator(true))) + .setValidationManager(new ValidationManager(new ArrayList<>())) .setSecurityMaskingRule(new HashSet<>()) .addSecurityMaskingRule("secret") .setSecurityMask("&&&&") @@ -334,7 +340,8 @@ public void testNullResultsForNodeIgnored() throws GestaltException { new DecoderRegistry(List.of(new DoubleDecoder(), new LongDecoder(), new IntegerDecoder(), new StringDecoder(), new ObjectDecoder()), configNodeManager, lexer, List.of(new StandardPathMapper())), - lexer, config, new ConfigNodeManager(), null, Collections.emptyList(), secretConcealer, null, Tags.of()); + lexer, config, new ConfigNodeManager(), null, Collections.emptyList(), secretConcealer, + null, null, Tags.of()); gestalt.loadConfigs(); diff --git a/gestalt-core/src/test/java/org/github/gestalt/config/validation/TestConfigValidator.java b/gestalt-core/src/test/java/org/github/gestalt/config/validation/TestConfigValidator.java new file mode 100644 index 000000000..347b3a811 --- /dev/null +++ b/gestalt-core/src/test/java/org/github/gestalt/config/validation/TestConfigValidator.java @@ -0,0 +1,30 @@ +package org.github.gestalt.config.validation; + +import org.github.gestalt.config.entity.ValidationError; +import org.github.gestalt.config.entity.ValidationLevel; +import org.github.gestalt.config.reflect.TypeCapture; +import org.github.gestalt.config.tag.Tags; +import org.github.gestalt.config.utils.GResultOf; + +public class TestConfigValidator implements ConfigValidator { + + public boolean isOk = true; + + public TestConfigValidator(boolean isOk) { + this.isOk = isOk; + } + + @Override + public GResultOf validator(T obj, String path, TypeCapture klass, Tags tags) { + if (isOk) { + return GResultOf.result(obj); + } else { + return GResultOf.errors(new ValidationError(ValidationLevel.ERROR) { + @Override + public String description() { + return "something broke"; + } + }); + } + } +} diff --git a/gestalt-core/src/test/java/org/github/gestalt/config/validation/ValidationManagerTest.java b/gestalt-core/src/test/java/org/github/gestalt/config/validation/ValidationManagerTest.java new file mode 100644 index 000000000..0fcd66742 --- /dev/null +++ b/gestalt-core/src/test/java/org/github/gestalt/config/validation/ValidationManagerTest.java @@ -0,0 +1,83 @@ +package org.github.gestalt.config.validation; + +import org.github.gestalt.config.entity.ValidationLevel; +import org.github.gestalt.config.reflect.TypeCapture; +import org.github.gestalt.config.tag.Tags; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.util.List; + +class ValidationManagerTest { + + @Test + public void testValidatorOk() { + var validationManager = new ValidationManager(List.of(new TestConfigValidator(true))); + + var results = validationManager.validator("test", "my.path", TypeCapture.of(String.class), Tags.of()); + + Assertions.assertTrue(results.hasResults()); + Assertions.assertFalse(results.hasErrors()); + + Assertions.assertEquals("test", results.results()); + } + + @Test + public void testTwoValidatorOk() { + var validationManager = new ValidationManager(List.of(new TestConfigValidator(true), new TestConfigValidator(true))); + + var results = validationManager.validator("test", "my.path", TypeCapture.of(String.class), Tags.of()); + + Assertions.assertTrue(results.hasResults()); + Assertions.assertFalse(results.hasErrors()); + + Assertions.assertEquals("test", results.results()); + } + + @Test + public void testValidatorOneError() { + var validationManager = new ValidationManager(List.of(new TestConfigValidator(false))); + + var results = validationManager.validator("test", "my.path", TypeCapture.of(String.class), Tags.of()); + + Assertions.assertFalse(results.hasResults()); + Assertions.assertTrue(results.hasErrors()); + + Assertions.assertEquals(1, results.getErrors().size()); + Assertions.assertEquals(ValidationLevel.ERROR, results.getErrors().get(0).level()); + Assertions.assertEquals("something broke", results.getErrors().get(0).description()); + } + + @Test + public void testTwoValidatorOneError() { + var validationManager = new ValidationManager(List.of(new TestConfigValidator(true), new TestConfigValidator(false))); + + var results = validationManager.validator("test", "my.path", TypeCapture.of(String.class), Tags.of()); + + Assertions.assertFalse(results.hasResults()); + Assertions.assertTrue(results.hasErrors()); + + Assertions.assertEquals(1, results.getErrors().size()); + Assertions.assertEquals(ValidationLevel.ERROR, results.getErrors().get(0).level()); + Assertions.assertEquals("something broke", results.getErrors().get(0).description()); + } + + @Test + public void testTwoValidatorTwoError() { + var validationManager = new ValidationManager(List.of(new TestConfigValidator(false), new TestConfigValidator(false))); + + var results = validationManager.validator("test", "my.path", TypeCapture.of(String.class), Tags.of()); + + Assertions.assertFalse(results.hasResults()); + Assertions.assertTrue(results.hasErrors()); + + Assertions.assertEquals(2, results.getErrors().size()); + + Assertions.assertEquals(ValidationLevel.ERROR, results.getErrors().get(0).level()); + Assertions.assertEquals("something broke", results.getErrors().get(0).description()); + + Assertions.assertEquals(ValidationLevel.ERROR, results.getErrors().get(1).level()); + Assertions.assertEquals("something broke", results.getErrors().get(1).description()); + } + +} diff --git a/gestalt-examples/gestalt-sample-module/build.gradle.kts b/gestalt-examples/gestalt-sample-module/build.gradle.kts index a18c3a10b..9f9bb56c9 100644 --- a/gestalt-examples/gestalt-sample-module/build.gradle.kts +++ b/gestalt-examples/gestalt-sample-module/build.gradle.kts @@ -18,14 +18,25 @@ dependencies { implementation(libs.gestalt.guice) implementation(libs.gestalt.hocon) implementation(libs.gestalt.kotlin) + implementation(libs.gestalt.micrometer) implementation(libs.gestalt.json) implementation(libs.gestalt.toml) + implementation(libs.gestalt.hibernate) implementation(libs.gestalt.vault) implementation(libs.gestalt.yaml) implementation(libs.junitAPI) + implementation(libs.assertJ) implementation(libs.aws.mock) implementation(libs.guice) + + implementation(libs.micrometer) + + implementation(libs.hibernate.validator) + implementation(libs.expressly) + + testImplementation(libs.junitAPI) + testImplementation(libs.assertJ) } java { diff --git a/gestalt-examples/gestalt-sample-module/src/main/java/module-info.java b/gestalt-examples/gestalt-sample-module/src/main/java/module-info.java index 55b2e6d60..a71c57693 100644 --- a/gestalt-examples/gestalt-sample-module/src/main/java/module-info.java +++ b/gestalt-examples/gestalt-sample-module/src/main/java/module-info.java @@ -2,29 +2,43 @@ * Module info definition for gestalt yaml integration */ module org.github.gestalt.config.integration { + + requires org.github.gestalt.aws; requires org.github.gestalt.core; + requires org.github.gestalt.git; + requires org.github.gestalt.google; requires org.github.gestalt.guice; requires org.github.gestalt.hocon; requires org.github.gestalt.json; - requires org.github.gestalt.yaml; requires org.github.gestalt.config.kotlin; + requires org.github.gestalt.micrometer; + requires org.github.gestalt.toml; + requires org.github.gestalt.validation.hibernate; + requires org.github.gestalt.vault; + requires org.github.gestalt.yaml; requires com.google.guice; requires org.junit.jupiter.api; requires kotlin.stdlib; requires software.amazon.awssdk.http; requires software.amazon.awssdk.services.s3; - requires org.github.gestalt.git; requires jakarta.inject; - requires org.github.gestalt.google; requires software.amazon.awssdk.auth; requires software.amazon.awssdk.regions; requires software.amazon.awssdk.http.urlconnection; - requires org.github.gestalt.aws; - requires org.github.gestalt.vault; + + requires jakarta.validation; + requires jakarta.el; + requires org.hibernate.validator; + requires com.fasterxml.classmate; + requires net.bytebuddy; + + requires vault.java.driver; + requires org.assertj.core; exports org.github.gestalt.config.integration; //you need to export any data classes you want to use the Object decoder on (since it uses reflection). - opens org.github.gestalt.config.integration to org.github.gestalt.core, org.github.gestalt.guice, org.github.gestalt.config.kotlin; + opens org.github.gestalt.config.integration to org.github.gestalt.core, org.github.gestalt.guice, + org.github.gestalt.config.kotlin, org.hibernate.validator; } diff --git a/gestalt-examples/gestalt-sample-module/src/main/java/org/github/gestalt/config/integration/GestaltConfigTest.java b/gestalt-examples/gestalt-sample-module/src/main/java/org/github/gestalt/config/integration/GestaltConfigTest.java index c454b85d3..ed47443a8 100644 --- a/gestalt-examples/gestalt-sample-module/src/main/java/org/github/gestalt/config/integration/GestaltConfigTest.java +++ b/gestalt-examples/gestalt-sample-module/src/main/java/org/github/gestalt/config/integration/GestaltConfigTest.java @@ -1,10 +1,19 @@ package org.github.gestalt.config.integration; +import com.github.gestalt.config.validation.hibernate.builder.HibernateModuleBuilder; import com.google.inject.Guice; import com.google.inject.Injector; import io.github.jopenlibs.vault.Vault; import io.github.jopenlibs.vault.VaultConfig; import io.github.jopenlibs.vault.VaultException; +import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import org.github.gestalt.config.Gestalt; import org.github.gestalt.config.annotations.Config; import org.github.gestalt.config.annotations.ConfigPrefix; @@ -16,6 +25,7 @@ import org.github.gestalt.config.google.storage.GCSConfigSourceBuilder; import org.github.gestalt.config.guice.GestaltModule; import org.github.gestalt.config.guice.InjectConfig; +import org.github.gestalt.config.micrometer.builder.MicrometerModuleConfigBuilder; import org.github.gestalt.config.post.process.transform.RandomTransformer; import org.github.gestalt.config.post.process.transform.SystemPropertiesTransformer; import org.github.gestalt.config.post.process.transform.TransformerPostProcessor; @@ -26,6 +36,7 @@ import org.github.gestalt.config.vault.config.VaultBuilder; import org.github.gestalt.config.vault.config.VaultModuleConfig; import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; import software.amazon.awssdk.auth.credentials.DefaultCredentialsProvider; import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient; import software.amazon.awssdk.regions.Region; @@ -50,10 +61,10 @@ public void integrationTest() throws GestaltException { // The later ones layer on and over write any values in the previous GestaltBuilder builder = new GestaltBuilder(); Gestalt gestalt = builder - .addSource(ClassPathConfigSourceBuilder.builder().setResource("default.properties").build()) - .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.properties").build()) - .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) - .build(); + .addSource(ClassPathConfigSourceBuilder.builder().setResource("default.properties").build()) + .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.properties").build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .build(); // Load the configurations, this will thow exceptions if there are any errors. gestalt.loadConfigs(); @@ -73,11 +84,11 @@ public void integrationTestNoCache() throws GestaltException { // The later ones layer on and over write any values in the previous GestaltBuilder builder = new GestaltBuilder(); Gestalt gestalt = builder - .addSource(ClassPathConfigSourceBuilder.builder().setResource("default.properties").build()) - .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.properties").build()) - .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) - .useCacheDecorator(false) - .build(); + .addSource(ClassPathConfigSourceBuilder.builder().setResource("default.properties").build()) + .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.properties").build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .useCacheDecorator(false) + .build(); // Load the configurations, this will thow exceptions if there are any errors. gestalt.loadConfigs(); @@ -95,14 +106,14 @@ public void integrationTestTags() throws GestaltException { GestaltBuilder builder = new GestaltBuilder(); Gestalt gestalt = builder - .addSource(URLConfigSourceBuilder.builder().setSourceURL(fileURL).build()) - .addSource(ClassPathConfigSourceBuilder.builder() - .setResource("dev.properties") - .setTags(Tags.of("toy", "ball")) - .build()) - .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) - .addSource(StringConfigSourceBuilder.builder().setConfig("db.idleTimeout=123").setFormat("properties").build()) - .build(); + .addSource(URLConfigSourceBuilder.builder().setSourceURL(fileURL).build()) + .addSource(ClassPathConfigSourceBuilder.builder() + .setResource("dev.properties") + .setTags(Tags.of("toy", "ball")) + .build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addSource(StringConfigSourceBuilder.builder().setConfig("db.idleTimeout=123").setFormat("properties").build()) + .build(); gestalt.loadConfigs(); @@ -159,11 +170,11 @@ public void integrationTestEnvVars() throws GestaltException { GestaltBuilder builder = new GestaltBuilder(); Gestalt gestalt = builder - .addSource(URLConfigSourceBuilder.builder().setSourceURL(urlFile).build()) - .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.properties").build()) - .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) - .addSource(EnvironmentConfigSourceBuilder.builder().build()) - .build(); + .addSource(URLConfigSourceBuilder.builder().setSourceURL(urlFile).build()) + .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.properties").build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addSource(EnvironmentConfigSourceBuilder.builder().build()) + .build(); gestalt.loadConfigs(); @@ -185,10 +196,10 @@ public void integrationTestJson() throws GestaltException { GestaltBuilder builder = new GestaltBuilder(); Gestalt gestalt = builder - .addSource(ClassPathConfigSourceBuilder.builder().setResource("default.json").build()) - .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.json").build()) - .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) - .build(); + .addSource(ClassPathConfigSourceBuilder.builder().setResource("default.json").build()) + .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.json").build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .build(); gestalt.loadConfigs(); @@ -204,10 +215,10 @@ public void integrationTestYaml() throws GestaltException { GestaltBuilder builder = new GestaltBuilder(); Gestalt gestalt = builder - .addSource(ClassPathConfigSourceBuilder.builder().setResource("default.yml").build()) - .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.yml").build()) - .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) - .build(); + .addSource(ClassPathConfigSourceBuilder.builder().setResource("default.yml").build()) + .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.yml").build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .build(); gestalt.loadConfigs(); @@ -223,10 +234,10 @@ public void integrationTestJsonAndYaml() throws GestaltException { GestaltBuilder builder = new GestaltBuilder(); Gestalt gestalt = builder - .addSource(ClassPathConfigSourceBuilder.builder().setResource("default.json").build()) - .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.yml").build()) - .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) - .build(); + .addSource(ClassPathConfigSourceBuilder.builder().setResource("default.json").build()) + .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.yml").build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .build(); gestalt.loadConfigs(); @@ -242,10 +253,10 @@ public void integrationTestHocon() throws GestaltException { GestaltBuilder builder = new GestaltBuilder(); Gestalt gestalt = builder - .addSource(ClassPathConfigSourceBuilder.builder().setResource("default.conf").build()) - .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.yml").build()) - .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) - .build(); + .addSource(ClassPathConfigSourceBuilder.builder().setResource("default.conf").build()) + .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.yml").build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .build(); gestalt.loadConfigs(); @@ -261,10 +272,10 @@ public void integrationTestToml() throws GestaltException { GestaltBuilder builder = new GestaltBuilder(); Gestalt gestalt = builder - .addSource(ClassPathConfigSourceBuilder.builder().setResource("default.conf").build()) - .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.toml").build()) - .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) - .build(); + .addSource(ClassPathConfigSourceBuilder.builder().setResource("default.conf").build()) + .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.toml").build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .build(); gestalt.loadConfigs(); @@ -283,17 +294,17 @@ public void integrationGitTest() throws GestaltException, IOException { GitConfigSourceBuilder gitBuilder = GitConfigSourceBuilder.builder() - .setRepoURI("https://github.com/gestalt-config/gestalt.git") - .setConfigFilePath("gestalt-examples/gestalt-sample/src/test/resources/default.properties") - .setLocalRepoDirectory(configDirectory); + .setRepoURI("https://github.com/gestalt-config/gestalt.git") + .setConfigFilePath("gestalt-examples/gestalt-sample/src/test/resources/default.properties") + .setLocalRepoDirectory(configDirectory); ConfigSourcePackage source = gitBuilder.build(); GestaltBuilder builder = new GestaltBuilder(); Gestalt gestalt = builder - .addSource(source) - .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.properties").build()) - .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) - .build(); + .addSource(source) + .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.properties").build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .build(); gestalt.loadConfigs(); @@ -336,10 +347,10 @@ public void integrationTestGoogleCloud() throws GestaltException { // The later ones layer on and over write any values in the previous GestaltBuilder builder = new GestaltBuilder(); Gestalt gestalt = builder - .addSource(ClassPathConfigSourceBuilder.builder().setResource("/default.properties").build()) - .addSource(GCSConfigSourceBuilder.builder().setBucketName("gestalt-test").setObjectName("dev.properties").build()) - .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) - .build(); + .addSource(ClassPathConfigSourceBuilder.builder().setResource("/default.properties").build()) + .addSource(GCSConfigSourceBuilder.builder().setBucketName("gestalt-test").setObjectName("dev.properties").build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .build(); // Load the configurations, this will thow exceptions if there are any errors. gestalt.loadConfigs(); @@ -385,25 +396,25 @@ public void integrationTestAws() throws GestaltException { admin.overrideEnabled=true */ S3Client s3Client = S3Client.builder() - .credentialsProvider(DefaultCredentialsProvider.create()) - .region(Region.US_EAST_1) - .httpClient(UrlConnectionHttpClient.builder().build()) - .build(); + .credentialsProvider(DefaultCredentialsProvider.create()) + .region(Region.US_EAST_1) + .httpClient(UrlConnectionHttpClient.builder().build()) + .build(); // using the builder to layer on the configuration files. // The later ones layer on and over write any values in the previous GestaltBuilder builder = new GestaltBuilder(); Gestalt gestalt = builder - .addSource(ClassPathConfigSourceBuilder.builder().setResource("/default.properties").build()) - .addSource(S3ConfigSourceBuilder.builder() - .setS3(s3Client) - .setBucketName("gestalt-test") - .setKeyName("dev.properties") - .build()) - .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) - .addModuleConfig(AWSBuilder.builder().setRegion("us-east-1").build()) - .build(); + .addSource(ClassPathConfigSourceBuilder.builder().setResource("/default.properties").build()) + .addSource(S3ConfigSourceBuilder.builder() + .setS3(s3Client) + .setBucketName("gestalt-test") + .setKeyName("dev.properties") + .build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addModuleConfig(AWSBuilder.builder().setRegion("us-east-1").build()) + .build(); // Load the configurations, this will thow exceptions if there are any errors. gestalt.loadConfigs(); @@ -423,9 +434,9 @@ public void integrationTestPostProcessorVault() throws GestaltException, VaultEx final VaultConfig config = new VaultConfig() - .address("http://127.0.0.1:8080") - .token(VAULT_TOKEN) - .build(); + .address("http://127.0.0.1:8080") + .token(VAULT_TOKEN) + .build(); Vault vault = Vault.create(config); @@ -438,11 +449,11 @@ public void integrationTestPostProcessorVault() throws GestaltException, VaultEx GestaltBuilder builder = new GestaltBuilder(); Gestalt gestalt = builder - .addSource(ClassPathConfigSourceBuilder.builder().setResource("/defaultPPVault.properties").build()) - .addSource(ClassPathConfigSourceBuilder.builder().setResource("/integration.properties").build()) - .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) - .addModuleConfig(vaultModuleConfig) - .build(); + .addSource(ClassPathConfigSourceBuilder.builder().setResource("/defaultPPVault.properties").build()) + .addSource(ClassPathConfigSourceBuilder.builder().setResource("/integration.properties").build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addModuleConfig(vaultModuleConfig) + .build(); gestalt.loadConfigs(); @@ -484,7 +495,7 @@ private void validateResults(Gestalt gestalt) throws GestaltException { Assertions.assertEquals(600, gestalt.getConfig("DB.connectionTimeout", Integer.class)); Assertions.assertEquals(123, db.idleTimeout); Assertions.assertEquals(60000.0F, db.maxLifetime); - Assertions.assertNull(db.isEnabled); + Assertions.assertTrue(db.isEnabled); Assertions.assertTrue(gestalt.getConfig("db.isEnabled", true, Boolean.class)); Assertions.assertEquals(3, db.hosts.size()); @@ -508,7 +519,7 @@ private void validateResults(Gestalt gestalt) throws GestaltException { Assertions.assertEquals(600, dbPrefix.connectionTimeout); Assertions.assertEquals(123, dbPrefix.idleTimeout); Assertions.assertEquals(60000.0F, dbPrefix.maxLifetime); - Assertions.assertNull(dbPrefix.isEnabled); + Assertions.assertTrue(dbPrefix.isEnabled); Assertions.assertEquals(3, dbPrefix.hosts.size()); Assertions.assertEquals("credmond", dbPrefix.hosts.getFirst().getUser()); @@ -635,7 +646,7 @@ private void validateResults(Gestalt gestalt) throws GestaltException { Assertions.assertEquals(600, gestalt.getConfig("DB.connectionTimeout", Integer.class)); Assertions.assertEquals(123, db.idleTimeout); Assertions.assertEquals(60000.0F, db.maxLifetime); - Assertions.assertNull(db.isEnabled); + Assertions.assertTrue(db.isEnabled); Assertions.assertTrue(gestalt.getConfig("db.isEnabled", true, Boolean.class)); Assertions.assertEquals(3, db.hosts.size()); @@ -673,11 +684,11 @@ public void integrationTestPostProcessorEnvironment() throws GestaltException { GestaltBuilder builder = new GestaltBuilder(); Gestalt gestalt = builder - .addSource(ClassPathConfigSourceBuilder.builder().setResource("defaultPPEnv.properties").build()) - .addSource(ClassPathConfigSourceBuilder.builder().setResource("integration.properties").build()) - .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) - .addDefaultPostProcessors() - .build(); + .addSource(ClassPathConfigSourceBuilder.builder().setResource("defaultPPEnv.properties").build()) + .addSource(ClassPathConfigSourceBuilder.builder().setResource("integration.properties").build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addDefaultPostProcessors() + .build(); gestalt.loadConfigs(); @@ -712,11 +723,11 @@ public void integrationTestPostProcessorSystem() throws GestaltException { GestaltBuilder builder = new GestaltBuilder(); Gestalt gestalt = builder - .addSource(ClassPathConfigSourceBuilder.builder().setResource("defaultPPSys.properties").build()) - .addSource(ClassPathConfigSourceBuilder.builder().setResource("integration.properties").build()) - .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) - .addPostProcessor(new TransformerPostProcessor(List.of(new SystemPropertiesTransformer(), new RandomTransformer()))) - .build(); + .addSource(ClassPathConfigSourceBuilder.builder().setResource("defaultPPSys.properties").build()) + .addSource(ClassPathConfigSourceBuilder.builder().setResource("integration.properties").build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .addPostProcessor(new TransformerPostProcessor(List.of(new SystemPropertiesTransformer(), new RandomTransformer()))) + .build(); gestalt.loadConfigs(); @@ -729,7 +740,7 @@ public void integrationTestPostProcessorSystem() throws GestaltException { Assertions.assertEquals("booking", booking.getService().getPath()); Assertions.assertNotNull(gestalt.getConfig("appUUID", UUID.class)); Assertions.assertTrue(gestalt.getConfig("appId", Integer.class) == 20 || - gestalt.getConfig("appId", Integer.class) == 21); + gestalt.getConfig("appId", Integer.class) == 21); } public void integrationTestPostProcessorNode() throws GestaltException { @@ -742,10 +753,10 @@ public void integrationTestPostProcessorNode() throws GestaltException { GestaltBuilder builder = new GestaltBuilder(); Gestalt gestalt = builder - .addSource(ClassPathConfigSourceBuilder.builder().setResource("defaultPPNode.properties").build()) - .addSource(ClassPathConfigSourceBuilder.builder().setResource("integration.properties").build()) - .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) - .build(); + .addSource(ClassPathConfigSourceBuilder.builder().setResource("defaultPPNode.properties").build()) + .addSource(ClassPathConfigSourceBuilder.builder().setResource("integration.properties").build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .build(); gestalt.loadConfigs(); @@ -769,8 +780,8 @@ public void integrationTestCamelCase() throws GestaltException { GestaltBuilder builder = new GestaltBuilder(); Gestalt gestalt = builder - .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) - .build(); + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .build(); gestalt.loadConfigs(); @@ -781,6 +792,161 @@ public void integrationTestCamelCase() throws GestaltException { } + @Test + public void testSubstitution() throws GestaltException { + Map customMap = new HashMap<>(); + customMap.put("place", "world"); + customMap.put("weather", "sunny"); + customMap.put("message", "hello ${place} it is ${weather} today"); + + GestaltBuilder builder = new GestaltBuilder(); + Gestalt gestalt = builder + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(customMap).build()) + .build(); + + gestalt.loadConfigs(); + + String message = gestalt.getConfig("message", TypeCapture.of(String.class)); + + Assertions.assertEquals("hello world it is sunny today", message); + } + + @Test + public void testNestedSubstitution() throws GestaltException { + Map customMap = new HashMap<>(); + customMap.put("variable", "place"); + customMap.put("place", "world"); + customMap.put("weather", "sunny"); + customMap.put("message", "hello ${${variable}} it is ${weather} today"); + + GestaltBuilder builder = new GestaltBuilder(); + Gestalt gestalt = builder + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(customMap).build()) + .build(); + + gestalt.loadConfigs(); + + String message = gestalt.getConfig("message", TypeCapture.of(String.class)); + + Assertions.assertEquals("hello world it is sunny today", message); + } + + @Test + public void testEscapedSubstitution() throws GestaltException { + Map customMap = new HashMap<>(); + customMap.put("place", "world"); + customMap.put("weather", "sunny"); + customMap.put("message", "hello \\${place} it is ${weather} today"); + + GestaltBuilder builder = new GestaltBuilder(); + Gestalt gestalt = builder + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(customMap).build()) + .build(); + + gestalt.loadConfigs(); + + String message = gestalt.getConfig("message", TypeCapture.of(String.class)); + + Assertions.assertEquals("hello ${place} it is sunny today", message); + } + + @Test + public void testMetrics() throws GestaltException { + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "123"); + configs.put("db.uri", "my.sql.com"); + + SimpleMeterRegistry registry = new SimpleMeterRegistry(); + + GestaltBuilder builder = new GestaltBuilder(); + Gestalt gestalt = builder + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .setMetricsEnabled(true) + .addModuleConfig(MicrometerModuleConfigBuilder.builder() + .setMeterRegistry(registry) + .setPrefix("myApp") + .build()) + .build(); + + gestalt.loadConfigs(); + + Assertions.assertEquals("test", gestalt.getConfig("db.password", String.class)); + + org.assertj.core.api.Assertions.assertThat(registry.getMetersAsString()) + .startsWith("myApp.config.get(TIMER)[]; count=1.0, total_time="); + + Assertions.assertEquals("test", gestalt.getConfig("db.password", String.class)); + + org.assertj.core.api.Assertions.assertThat(registry.getMetersAsString()) + .contains("myApp.config.get(TIMER)[]; count=1.0, total_time=") + .contains("myApp.cache.hit(COUNTER)[]; count=1.0"); + } + + @Test + public void testValidationOk() throws GestaltException { + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "123"); + configs.put("db.uri", "my.sql.com"); + + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + Validator validator = factory.getValidator(); + + GestaltBuilder builder = new GestaltBuilder(); + Gestalt gestalt = builder + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .setValidationEnabled(true) + .addModuleConfig(HibernateModuleBuilder.builder() + .setValidator(validator) + .build()) + .build(); + + gestalt.loadConfigs(); + + Assertions.assertEquals("test", gestalt.getConfig("db.password", String.class)); + Assertions.assertEquals("test", gestalt.getConfig("db", DBInfo.class).password); + Assertions.assertEquals(123, gestalt.getConfig("db", DBInfo.class).port); + + Assertions.assertEquals("test", gestalt.getConfig("db", DBInfoValid.class).password); + Assertions.assertEquals(123, gestalt.getConfig("db", DBInfoValid.class).port); + } + + @Test + public void testValidationError() throws GestaltException { + Map configs = new HashMap<>(); + configs.put("db.password", "12345678901234567890"); + configs.put("db.port", "0"); + configs.put("db.uri", "my.sql.com"); + + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + Validator validator = factory.getValidator(); + + GestaltBuilder builder = new GestaltBuilder(); + Gestalt gestalt = builder + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .setValidationEnabled(true) + .addModuleConfig(HibernateModuleBuilder.builder() + .setValidator(validator) + .build()) + .build(); + + gestalt.loadConfigs(); + + Assertions.assertEquals("12345678901234567890", gestalt.getConfig("db.password", String.class)); + Assertions.assertEquals("12345678901234567890", gestalt.getConfig("db", DBInfo.class).password); + Assertions.assertEquals(0, gestalt.getConfig("db", DBInfo.class).port); + + var ex = Assertions.assertThrows(GestaltException.class, () -> gestalt.getConfig("db", DBInfoValid.class)); + + org.assertj.core.api.Assertions.assertThat(ex.getMessage()) + .startsWith("Validation failed for config path: db, and " + + "class: org.github.gestalt.config.integration.GestaltConfigTest$DBInfoValid") + .contains("- level: ERROR, message: Hibernate Validator, on path: db, error: size must be between 2 and 14") + .contains("- level: ERROR, message: Hibernate Validator, on path: db, error: port should not be less than 10"); + } + + public enum Role { LEVEL0, LEVEL1 } @@ -924,7 +1090,7 @@ public static class DataBase { public int connectionTimeout; public Integer idleTimeout; public float maxLifetime; - public Boolean isEnabled; + public Boolean isEnabled = true; public DataBase() { @@ -937,7 +1103,7 @@ public static class DataBasePrefix { public int connectionTimeout; public Integer idleTimeout; public float maxLifetime; - public Boolean isEnabled; + public Boolean isEnabled = true; public DataBasePrefix() { } @@ -1042,4 +1208,97 @@ public void setDataBase(DataBase dataBase) { this.dataBase = dataBase; } } + + + public static class DBInfo { + private Integer port; + private String uri; + private String password; + @Config(defaultVal = "200") + private Integer connections; + + public DBInfo() { + } + + public Integer getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Integer getConnections() { + return connections; + } + + public void setConnections(Integer connections) { + this.connections = connections; + } + } + + public static class DBInfoValid { + + @Min(value = 10, message = "port should not be less than 10") + @Max(value = 200, message = "port should not be greater than 200") + private Integer port; + private String uri; + @NotNull + @Size(min = 2, max = 14) + private String password; + @NotNull + @Config(defaultVal = "200") + private Integer connections; + + public DBInfoValid() { + } + + public Integer getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Integer getConnections() { + return connections; + } + + public void setConnections(Integer connections) { + this.connections = connections; + } + } } diff --git a/gestalt-examples/gestalt-sample-module/src/main/java/org/github/gestalt/config/integration/MainClass.java b/gestalt-examples/gestalt-sample-module/src/main/java/org/github/gestalt/config/integration/MainClass.java index 03328d56a..2ebfb3b5f 100644 --- a/gestalt-examples/gestalt-sample-module/src/main/java/org/github/gestalt/config/integration/MainClass.java +++ b/gestalt-examples/gestalt-sample-module/src/main/java/org/github/gestalt/config/integration/MainClass.java @@ -26,6 +26,10 @@ public static void main(String[] args) throws GestaltException, IOException, Vau configTest.integrationTestPostProcessorSystem(); configTest.integrationTestPostProcessorNode(); configTest.integrationTestCamelCase(); + configTest.testEscapedSubstitution(); + configTest.testMetrics(); + configTest.testValidationOk(); + configTest.testValidationError(); if( Boolean.parseBoolean(System.getenv("LOCAL_TEST"))) { configTest.integrationTestGoogleCloud(); diff --git a/gestalt-examples/gestalt-sample-module/src/main/kotlin/org/github/gestalt/config/integration/GestaltKotlinTest.kt b/gestalt-examples/gestalt-sample-module/src/main/kotlin/org/github/gestalt/config/integration/GestaltKotlinTest.kt index aed4f5739..cd99ed256 100644 --- a/gestalt-examples/gestalt-sample-module/src/main/kotlin/org/github/gestalt/config/integration/GestaltKotlinTest.kt +++ b/gestalt-examples/gestalt-sample-module/src/main/kotlin/org/github/gestalt/config/integration/GestaltKotlinTest.kt @@ -26,6 +26,7 @@ class GestaltKotlinTest { .addSource(ClassPathConfigSourceBuilder.builder().setResource("default.properties").build()) .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.properties").build()) .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .setTreatMissingValuesAsErrors(false) .build() gestalt.loadConfigs() testValidation(gestalt) @@ -43,6 +44,7 @@ class GestaltKotlinTest { .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.properties").build()) .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) .addSource(EnvironmentConfigSourceBuilder.builder().build()) + .setTreatMissingValuesAsErrors(false) .build() gestalt.loadConfigs() testValidation(gestalt) @@ -67,6 +69,7 @@ class GestaltKotlinTest { .addSource(ClassPathConfigSourceBuilder.builder().setResource("default.properties").build()) .addSource(ClassPathConfigSourceBuilder.builder().setResource("dev.properties").build()) .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .setTreatMissingValuesAsErrors(false) .build() gestalt.loadConfigs() val pool: HttpPool = gestalt.getConfig("http.pool", KTypeCapture.of(typeOf())) as HttpPool @@ -206,7 +209,7 @@ class GestaltKotlinTest { var connectionTimeout = 0 var idleTimeout: Int? = null var maxLifetime = 0f - var isEnabled: Boolean? = null + var isEnabled: Boolean? = null; } class User { diff --git a/gestalt-examples/gestalt-sample-module/src/test/java/org/github/gestalt/config/integration/GestaltSampleTest.java b/gestalt-examples/gestalt-sample-module/src/test/java/org/github/gestalt/config/integration/GestaltSampleTest.java new file mode 100644 index 000000000..e08ac4f83 --- /dev/null +++ b/gestalt-examples/gestalt-sample-module/src/test/java/org/github/gestalt/config/integration/GestaltSampleTest.java @@ -0,0 +1,47 @@ +package org.github.gestalt.config.integration; + +import io.github.jopenlibs.vault.VaultException; +import org.github.gestalt.config.exceptions.GestaltException; +import org.junit.Test; + +import java.io.IOException; + +public class GestaltSampleTest { + + @Test + public void testIntegration() throws GestaltException, IOException, VaultException { + GestaltConfigTest configTest = new GestaltConfigTest(); + configTest.integrationTest(); + configTest.integrationTestNoCache(); + configTest.integrationTestTags(); + configTest.integrationTestEnvVars(); + configTest.integrationTestJson(); + configTest.integrationTestYaml(); + configTest.integrationTestJsonAndYaml(); + configTest.integrationTestHocon(); + configTest.integrationTestToml(); + configTest.integrationGitTest(); + configTest.integrationTestPostProcessorEnvironment(); + configTest.integrationTestPostProcessorSystem(); + configTest.integrationTestPostProcessorNode(); + configTest.integrationTestCamelCase(); + configTest.testEscapedSubstitution(); + configTest.testMetrics(); + configTest.testValidationOk(); + configTest.testValidationError(); + + if( Boolean.parseBoolean(System.getenv("LOCAL_TEST"))) { + configTest.integrationTestGoogleCloud(); + configTest.integrationTestAws(); + } + + if( Boolean.parseBoolean(System.getenv("GESTALT_VAULT_TEST"))) { + configTest.integrationTestPostProcessorVault(); + } + + var kotlinTests = new GestaltKotlinTest(); + kotlinTests.integrationTest(); + kotlinTests.integrationTestEnvVars(); + kotlinTests.integrationTestWithTypeOf(); + } +} diff --git a/gestalt-examples/gestalt-sample/build.gradle.kts b/gestalt-examples/gestalt-sample/build.gradle.kts index e917f3867..ab5db41ec 100644 --- a/gestalt-examples/gestalt-sample/build.gradle.kts +++ b/gestalt-examples/gestalt-sample/build.gradle.kts @@ -41,6 +41,7 @@ testing { implementation(project(":gestalt-micrometer")) implementation(project(":gestalt-json")) implementation(project(":gestalt-toml")) + implementation(project(":gestalt-validator-hibernate")) implementation(project(":gestalt-vault")) implementation(project(":gestalt-yaml")) @@ -54,6 +55,10 @@ testing { implementation(project(":gestalt-guice")) implementation(libs.testcontainers.junit5) implementation(libs.micrometer) + + implementation(libs.hibernate.validator) + + implementation(libs.expressly) } } } diff --git a/gestalt-examples/gestalt-sample/src/test/java/org/github/gestalt/config/integration/GestaltSample.java b/gestalt-examples/gestalt-sample/src/test/java/org/github/gestalt/config/integration/GestaltSample.java index ba0386811..ecbf36156 100644 --- a/gestalt-examples/gestalt-sample/src/test/java/org/github/gestalt/config/integration/GestaltSample.java +++ b/gestalt-examples/gestalt-sample/src/test/java/org/github/gestalt/config/integration/GestaltSample.java @@ -2,6 +2,7 @@ import com.adobe.testing.s3mock.testcontainers.S3MockContainer; +import com.github.gestalt.config.validation.hibernate.builder.HibernateModuleBuilder; import com.google.inject.Guice; import com.google.inject.Injector; import io.github.jopenlibs.vault.Vault; @@ -9,6 +10,13 @@ import io.github.jopenlibs.vault.VaultException; import io.github.jopenlibs.vault.response.LogicalResponse; import io.micrometer.core.instrument.simple.SimpleMeterRegistry; +import jakarta.validation.Validation; +import jakarta.validation.Validator; +import jakarta.validation.ValidatorFactory; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; import org.github.gestalt.config.Gestalt; import org.github.gestalt.config.annotations.Config; import org.github.gestalt.config.annotations.ConfigPrefix; @@ -1391,6 +1399,67 @@ public void testMetrics() throws GestaltException { .contains("myApp.cache.hit(COUNTER)[]; count=1.0"); } + @Test + public void testValidationOk() throws GestaltException { + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "123"); + configs.put("db.uri", "my.sql.com"); + + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + Validator validator = factory.getValidator(); + + GestaltBuilder builder = new GestaltBuilder(); + Gestalt gestalt = builder + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .setValidationEnabled(true) + .addModuleConfig(HibernateModuleBuilder.builder() + .setValidator(validator) + .build()) + .build(); + + gestalt.loadConfigs(); + + Assertions.assertEquals("test", gestalt.getConfig("db.password", String.class)); + Assertions.assertEquals("test", gestalt.getConfig("db", DBInfo.class).password); + Assertions.assertEquals(123, gestalt.getConfig("db", DBInfo.class).port); + + Assertions.assertEquals("test", gestalt.getConfig("db", DBInfoValid.class).password); + Assertions.assertEquals(123, gestalt.getConfig("db", DBInfoValid.class).port); + } + + @Test + public void testValidationError() throws GestaltException { + Map configs = new HashMap<>(); + configs.put("db.password", "12345678901234567890"); + configs.put("db.port", "0"); + configs.put("db.uri", "my.sql.com"); + + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + Validator validator = factory.getValidator(); + + Gestalt gestalt = new GestaltBuilder() + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .setValidationEnabled(true) + .addModuleConfig(HibernateModuleBuilder.builder() + .setValidator(validator) + .build()) + .build(); + + gestalt.loadConfigs(); + + Assertions.assertEquals("12345678901234567890", gestalt.getConfig("db.password", String.class)); + Assertions.assertEquals("12345678901234567890", gestalt.getConfig("db", DBInfo.class).password); + Assertions.assertEquals(0, gestalt.getConfig("db", DBInfo.class).port); + + var ex = Assertions.assertThrows(GestaltException.class, () -> gestalt.getConfig("db", DBInfoValid.class)); + org.assertj.core.api.Assertions.assertThat(ex.getMessage()) + .startsWith("Validation failed for config path: db, and " + + "class: org.github.gestalt.config.integration.GestaltSample$DBInfoValid") + .contains("- level: ERROR, message: Hibernate Validator, on path: db, error: size must be between 2 and 14") + .contains("- level: ERROR, message: Hibernate Validator, on path: db, error: port should not be less than 10"); + + } public enum Role { LEVEL0, LEVEL1 @@ -1726,4 +1795,100 @@ public void setMaxTotal(Integer port) { this.maxTotal = port; } } + + public static class DBInfo { + private Integer port; + private String uri; + private String password; + + public DBInfo() { + } + + @Config(defaultVal = "200") + private Integer connections; + + public Integer getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Integer getConnections() { + return connections; + } + + public void setConnections(Integer connections) { + this.connections = connections; + } + } + + public static class DBInfoValid { + + public DBInfoValid() { + } + + @Min(value = 10, message = "port should not be less than 10") + @Max(value = 200, message = "port should not be greater than 200") + private Integer port; + + private String uri; + + @NotNull + @Size(min = 2, max = 14) + private String password; + + @NotNull + @Config(defaultVal = "200") + private Integer connections; + + public Integer getPort() { + return port; + } + + public void setPort(int port) { + this.port = port; + } + + public String getUri() { + return uri; + } + + public void setUri(String uri) { + this.uri = uri; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public Integer getConnections() { + return connections; + } + + public void setConnections(Integer connections) { + this.connections = connections; + } + } } diff --git a/gestalt-micrometer/src/main/java/module-info.java b/gestalt-micrometer/src/main/java/module-info.java index d7bbf976c..2c4fe3d20 100644 --- a/gestalt-micrometer/src/main/java/module-info.java +++ b/gestalt-micrometer/src/main/java/module-info.java @@ -2,7 +2,7 @@ * Module info definition for gestalt yaml integration */ @SuppressWarnings({ "requires-transitive-automatic" }) -module org.github.gestalt.metrics { +module org.github.gestalt.micrometer { requires org.github.gestalt.core; requires transitive micrometer.core; diff --git a/gestalt-validator-hibernate/build.gradle.kts b/gestalt-validator-hibernate/build.gradle.kts new file mode 100644 index 000000000..d1f71e368 --- /dev/null +++ b/gestalt-validator-hibernate/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id("gestalt.java-library-conventions") + id("gestalt.java-test-conventions") + id("gestalt.java-code-quality-conventions") + id("gestalt.java-publish-conventions") +} + +dependencies { + implementation(project(":gestalt-core")) + api(libs.hibernate.validator) + + testImplementation(libs.expressly) +} + + diff --git a/gestalt-validator-hibernate/src/main/java/com/github/gestalt/config/validation/hibernate/HibernateConfigValidator.java b/gestalt-validator-hibernate/src/main/java/com/github/gestalt/config/validation/hibernate/HibernateConfigValidator.java new file mode 100644 index 000000000..0da594003 --- /dev/null +++ b/gestalt-validator-hibernate/src/main/java/com/github/gestalt/config/validation/hibernate/HibernateConfigValidator.java @@ -0,0 +1,80 @@ +package com.github.gestalt.config.validation.hibernate; + +import com.github.gestalt.config.validation.hibernate.config.HibernateModuleConfig; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.Validation; +import jakarta.validation.ValidatorFactory; +import org.github.gestalt.config.entity.GestaltConfig; +import org.github.gestalt.config.entity.ValidationError; +import org.github.gestalt.config.entity.ValidationLevel; +import org.github.gestalt.config.reflect.TypeCapture; +import org.github.gestalt.config.tag.Tags; +import org.github.gestalt.config.utils.GResultOf; +import org.github.gestalt.config.validation.ConfigValidator; + +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Implementation of the Gestalt validator for Hibernate. Used to validate configurations. + * + * @author Colin Redmond (c) 2024. + */ +public final class HibernateConfigValidator implements ConfigValidator { + + private static final System.Logger logger = System.getLogger(HibernateConfigValidator.class.getName()); + + private jakarta.validation.Validator validator; + + public HibernateConfigValidator() { + try (ValidatorFactory factory = Validation.buildDefaultValidatorFactory()) { + validator = factory.getValidator(); + } + } + + @Override + public void applyConfig(GestaltConfig config) { + HibernateModuleConfig moduleConfig = config.getModuleConfig(HibernateModuleConfig.class); + if (moduleConfig == null ) { + logger.log(System.Logger.Level.WARNING, "Hibernate Validator will be set to the defaults and use a " + + "Validation.buildDefaultValidatorFactory() to build a Validator, please register a HibernateModuleConfig with " + + "gestalt builder to customize the validation."); + + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + validator = factory.getValidator(); + } else { + + validator = moduleConfig.getValidator(); + } + } + + @Override + public GResultOf validator(T obj, String path, TypeCapture klass, Tags tags) { + Set> constraintViolations = validator.validate(obj); + + if (constraintViolations.isEmpty()) { + return GResultOf.result(obj); + } else { + return GResultOf.errors(constraintViolations.stream() + .map(ConstraintViolation::getMessage) + .map(it -> new HibernateValidatorError(path, it)) + .collect(Collectors.toList())); + } + } + + public static class HibernateValidatorError extends ValidationError { + private final String path; + private final String error; + + public HibernateValidatorError(String path, String error) { + super(ValidationLevel.ERROR); + this.path = path; + this.error = error; + } + + @Override + public String description() { + return "Hibernate Validator, on path: " + path + ", error: " + error; + } + } +} diff --git a/gestalt-validator-hibernate/src/main/java/com/github/gestalt/config/validation/hibernate/builder/HibernateModuleBuilder.java b/gestalt-validator-hibernate/src/main/java/com/github/gestalt/config/validation/hibernate/builder/HibernateModuleBuilder.java new file mode 100644 index 000000000..eed9623a9 --- /dev/null +++ b/gestalt-validator-hibernate/src/main/java/com/github/gestalt/config/validation/hibernate/builder/HibernateModuleBuilder.java @@ -0,0 +1,54 @@ +package com.github.gestalt.config.validation.hibernate.builder; + +import com.github.gestalt.config.validation.hibernate.config.HibernateModuleConfig; +import jakarta.validation.Validator; + +/** + * Builder for creating Vault specific configuration. + * You can either provide the HibernateModuleConfig and the builder will create the client, + * or you can provide a Vault client yourself. + * + * + * @author Colin Redmond (c) 2024. + */ +public final class HibernateModuleBuilder { + + private Validator validator; + + private HibernateModuleBuilder() { + + } + + public static HibernateModuleBuilder builder() { + return new HibernateModuleBuilder(); + } + + /** + * Get the jakarta validator. + * + * @return the jakarta validator + */ + public Validator getValidator() { + return validator; + } + + /** + * Set the jakarta validator. + * + * @param validator the jakarta validator + * @return builder + */ + public HibernateModuleBuilder setValidator(Validator validator) { + this.validator = validator; + return this; + } + + /** + * Build the HibernateModuleConfig. + * + * @return HibernateModuleConfig + */ + public HibernateModuleConfig build() { + return new HibernateModuleConfig(validator); + } +} diff --git a/gestalt-validator-hibernate/src/main/java/com/github/gestalt/config/validation/hibernate/config/HibernateModuleConfig.java b/gestalt-validator-hibernate/src/main/java/com/github/gestalt/config/validation/hibernate/config/HibernateModuleConfig.java new file mode 100644 index 000000000..8f475884d --- /dev/null +++ b/gestalt-validator-hibernate/src/main/java/com/github/gestalt/config/validation/hibernate/config/HibernateModuleConfig.java @@ -0,0 +1,38 @@ +package com.github.gestalt.config.validation.hibernate.config; + +import jakarta.validation.Validator; +import org.github.gestalt.config.entity.GestaltModuleConfig; + + +/** + * Module config for Hibernate. Provides a jakarta Validator to validate objects. + * + * @author Colin Redmond (c) 2024. + */ +public final class HibernateModuleConfig implements GestaltModuleConfig { + + private final Validator validator; + + /** + * Create the HibernateModuleConfig. + * + * @param validator jakarta Validator + */ + public HibernateModuleConfig(Validator validator) { + this.validator = validator; + } + + @Override + public String name() { + return "hibernate-validator"; + } + + /** + * Get the jakarta Validator. + * + * @return jakarta Validator + */ + public Validator getValidator() { + return validator; + } +} diff --git a/gestalt-validator-hibernate/src/main/java/module-info.java b/gestalt-validator-hibernate/src/main/java/module-info.java new file mode 100644 index 000000000..0855c8310 --- /dev/null +++ b/gestalt-validator-hibernate/src/main/java/module-info.java @@ -0,0 +1,16 @@ +/* + * Module info definition for gestalt yaml integration + */ +@SuppressWarnings({ "requires-transitive-automatic" }) +module org.github.gestalt.validation.hibernate { + requires org.github.gestalt.core; + requires transitive jakarta.validation; + requires transitive org.hibernate.validator; + + exports com.github.gestalt.config.validation.hibernate; + exports com.github.gestalt.config.validation.hibernate.config; + exports com.github.gestalt.config.validation.hibernate.builder; + + provides org.github.gestalt.config.validation.ConfigValidator with + com.github.gestalt.config.validation.hibernate.HibernateConfigValidator; +} diff --git a/gestalt-validator-hibernate/src/main/resources/META-INF/services/org.github.gestalt.config.validation.ConfigValidator b/gestalt-validator-hibernate/src/main/resources/META-INF/services/org.github.gestalt.config.validation.ConfigValidator new file mode 100644 index 000000000..db4bf2ae2 --- /dev/null +++ b/gestalt-validator-hibernate/src/main/resources/META-INF/services/org.github.gestalt.config.validation.ConfigValidator @@ -0,0 +1 @@ +com.github.gestalt.config.validation.hibernate.HibernateConfigValidator diff --git a/gestalt-validator-hibernate/src/test/java/com/github/gestalt/config/validation/hibernate/HibernateConfigValidatorTest.java b/gestalt-validator-hibernate/src/test/java/com/github/gestalt/config/validation/hibernate/HibernateConfigValidatorTest.java new file mode 100644 index 000000000..09cadef9c --- /dev/null +++ b/gestalt-validator-hibernate/src/test/java/com/github/gestalt/config/validation/hibernate/HibernateConfigValidatorTest.java @@ -0,0 +1,128 @@ +package com.github.gestalt.config.validation.hibernate; + +import com.github.gestalt.config.validation.hibernate.builder.HibernateModuleBuilder; +import jakarta.validation.Validation; +import jakarta.validation.ValidatorFactory; +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import org.github.gestalt.config.entity.GestaltConfig; +import org.github.gestalt.config.entity.ValidationLevel; +import org.github.gestalt.config.reflect.TypeCapture; +import org.github.gestalt.config.tag.Tags; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class HibernateConfigValidatorTest { + + @Test + public void testHibernateValidatorOk() { + + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + var validator = factory.getValidator(); + HibernateModuleBuilder builder = HibernateModuleBuilder.builder().setValidator(validator); + GestaltConfig gestaltConfig = new GestaltConfig(); + gestaltConfig.registerModuleConfig(builder.build()); + + HibernateConfigValidator hibernateValidator = new HibernateConfigValidator(); + + hibernateValidator.applyConfig(gestaltConfig); + + Car car = new Car("Morris", "DD-AB-123", 2); + + var results = hibernateValidator.validator(car, "car", TypeCapture.of(Car.class), Tags.of()); + + Assertions.assertTrue(results.hasResults()); + Assertions.assertFalse(results.hasErrors()); + } + + @Test + public void testHibernateValidatorErrors() { + + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + var validator = factory.getValidator(); + HibernateModuleBuilder builder = HibernateModuleBuilder.builder().setValidator(validator); + GestaltConfig gestaltConfig = new GestaltConfig(); + gestaltConfig.registerModuleConfig(builder.build()); + + HibernateConfigValidator hibernateValidator = new HibernateConfigValidator(); + + hibernateValidator.applyConfig(gestaltConfig); + + Car car = new Car("Morris", "DD-AB-123", 1); + + var results = hibernateValidator.validator(car, "car", TypeCapture.of(Car.class), Tags.of()); + + Assertions.assertFalse(results.hasResults()); + Assertions.assertTrue(results.hasErrors()); + Assertions.assertEquals(1, results.getErrors().size()); + Assertions.assertEquals(ValidationLevel.ERROR, results.getErrors().get(0).level()); + Assertions.assertEquals("Hibernate Validator, on path: car, error: must be greater than or equal to 2", + results.getErrors().get(0).description()); + } + + @Test + public void testHibernateValidatorMultipleErrors() { + + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + var validator = factory.getValidator(); + HibernateModuleBuilder builder = HibernateModuleBuilder.builder().setValidator(validator); + GestaltConfig gestaltConfig = new GestaltConfig(); + gestaltConfig.registerModuleConfig(builder.build()); + + HibernateConfigValidator hibernateValidator = new HibernateConfigValidator(); + + hibernateValidator.applyConfig(gestaltConfig); + + Car car = new Car("Morris", "A", 1); + + var results = hibernateValidator.validator(car, "car", TypeCapture.of(Car.class), Tags.of()); + + Assertions.assertFalse(results.hasResults()); + Assertions.assertTrue(results.hasErrors()); + Assertions.assertEquals(2, results.getErrors().size()); + + Assertions.assertEquals(ValidationLevel.ERROR, results.getErrors().get(0).level()); + + org.assertj.core.api.Assertions.assertThat(results.getErrors().get(0).description()).containsAnyOf( + "Hibernate Validator, on path: car, error: size must be between 2 and 14", + "Hibernate Validator, on path: car, error: must be greater than or equal to 2"); + + Assertions.assertEquals(ValidationLevel.ERROR, results.getErrors().get(1).level()); + org.assertj.core.api.Assertions.assertThat(results.getErrors().get(1).description()).containsAnyOf( + "Hibernate Validator, on path: car, error: size must be between 2 and 14", + "Hibernate Validator, on path: car, error: must be greater than or equal to 2"); + } + + public static class Car { + + @NotNull + private final String manufacturer; + + @NotNull + @Size(min = 2, max = 14) + private final String licensePlate; + + @Min(2) + private final int seatCount; + + public Car(String manufacturer, String licencePlate, int seatCount) { + this.manufacturer = manufacturer; + this.licensePlate = licencePlate; + this.seatCount = seatCount; + } + + public String getManufacturer() { + return manufacturer; + } + + public String getLicensePlate() { + return licensePlate; + } + + public int getSeatCount() { + return seatCount; + } + } +} + diff --git a/gestalt-validator-hibernate/src/test/java/com/github/gestalt/config/validation/hibernate/builder/HibernateModuleBuilderTest.java b/gestalt-validator-hibernate/src/test/java/com/github/gestalt/config/validation/hibernate/builder/HibernateModuleBuilderTest.java new file mode 100644 index 000000000..664ef062e --- /dev/null +++ b/gestalt-validator-hibernate/src/test/java/com/github/gestalt/config/validation/hibernate/builder/HibernateModuleBuilderTest.java @@ -0,0 +1,23 @@ +package com.github.gestalt.config.validation.hibernate.builder; + +import jakarta.validation.Validation; +import jakarta.validation.ValidatorFactory; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class HibernateModuleBuilderTest { + + @Test + public void builderTest() { + ValidatorFactory factory = Validation.buildDefaultValidatorFactory(); + var validator = factory.getValidator(); + HibernateModuleBuilder builder = HibernateModuleBuilder.builder().setValidator(validator); + + Assertions.assertEquals(validator, builder.getValidator()); + + var config = builder.build(); + Assertions.assertEquals(validator, config.getValidator()); + Assertions.assertEquals("hibernate-validator", config.name()); + } + +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ff4837b52..1af6cf621 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ kotlin = "1.9.23" kotlinDokka = "1.9.20" # Kotlin DI kodeinDI = "7.21.2" -koinDI = "3.5.3" +koinDI = "3.5.4" # Java DI guice = "7.0.0" # @pin @@ -20,8 +20,8 @@ weldCore = "4.0.3.Final" jackson = "2.17.0" hocon = "1.4.3" # Cloud -awsBom = "2.25.23" -gcpLibraries = "26.35.0" +awsBom = "2.25.26" +gcpLibraries = "26.37.0" # vault vault = "6.2.0" # Git support @@ -29,6 +29,9 @@ jgit = "6.9.0.202403050737-r" eddsa = "0.3.0" # metrics micrometer = "1.12.4" +# validation +hibernateValidator = "8.0.1.Final" +expressly = "5.0.0" # testing junit5 = "5.10.2" assertJ = "3.25.3" @@ -47,7 +50,7 @@ checkStyle = "10.10.0" jmh = "1.37" gradleJmh = "0.7.2" # @pin gestalt for integration tests. -gestalt = "0.25.3" +gestalt = "0.27.0" # Gradle utility gradleVersions = "0.51.0" gitVersions = "3.0.0" @@ -93,6 +96,9 @@ jgit-apache-SSH = { module = "org.eclipse.jgit:org.eclipse.jgit.ssh.apache", ver eddsa = { module = "net.i2p.crypto:eddsa", version.ref = "eddsa" } # metrics micrometer = { module = "io.micrometer:micrometer-core", version.ref = "micrometer" } +# configValidators +hibernate-validator = { module = "org.hibernate.validator:hibernate-validator", version.ref = "hibernateValidator" } +expressly = { module = "org.glassfish.expressly:expressly", version.ref = "expressly" } # Testing junitAPI = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit5" } junitEngine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit5" } @@ -127,7 +133,9 @@ gestalt-json = { module = "com.github.gestalt-config:gestalt-json", version.ref gestalt-kodein-di = { module = "com.github.gestalt-config:gestalt-kodein-di", version.ref = "gestalt" } gestalt-koin-di = { module = "com.github.gestalt-config:gestalt-koin-di", version.ref = "gestalt" } gestalt-kotlin = { module = "com.github.gestalt-config:gestalt-kotlin", version.ref = "gestalt" } +gestalt-micrometer = { module = "com.github.gestalt-config:gestalt-micrometer", version.ref = "gestalt" } gestalt-toml = { module = "com.github.gestalt-config:gestalt-toml", version.ref = "gestalt" } +gestalt-hibernate = { module = "com.github.gestalt-config:gestalt-validator-hibernate", version.ref = "gestalt" } gestalt-vault = { module = "com.github.gestalt-config:gestalt-vault", version.ref = "gestalt" } gestalt-yaml = { module = "com.github.gestalt-config:gestalt-yaml", version.ref = "gestalt" } diff --git a/settings.gradle.kts b/settings.gradle.kts index 1cc873349..111d10536 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -2,7 +2,7 @@ rootProject.name = "gestalt" include( "gestalt-aws", "gestalt-cdi", "gestalt-core", "gestalt-hocon", "gestalt-json", "gestalt-git", "gestalt-google-cloud", "gestalt-guice", "gestalt-kotlin", "gestalt-micrometer", "gestalt-kodein-di", - "gestalt-koin-di", "gestalt-toml", "gestalt-vault", "gestalt-yaml" + "gestalt-koin-di", "gestalt-toml", "gestalt-validator-hibernate", "gestalt-vault", "gestalt-yaml" ) // testing utility projects