Skip to content

Commit

Permalink
Merge pull request #178 from gestalt-config/feat/validation
Browse files Browse the repository at this point in the history
feat: add hooks to support object validation.
  • Loading branch information
credmond-git authored Apr 6, 2024
2 parents 5da5c05 + e6482bc commit a402730
Show file tree
Hide file tree
Showing 37 changed files with 1,882 additions and 156 deletions.
48 changes: 43 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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()
Expand Down Expand Up @@ -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
<dependency>
<groupId>com.github.gestalt-config</groupId>
<artifactId>gestalt-validator-hibernate</artifactId>
<version>${version}</version>
</dependency>
```
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.
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {

allprojects {
group = "com.github.gestalt-config"
version = "0.26.0"
version = "0.27.0"
}


3 changes: 3 additions & 0 deletions code-coverage-report/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
3 changes: 3 additions & 0 deletions gestalt-core/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.*;

Expand Down Expand Up @@ -61,6 +63,8 @@ public class GestaltCore implements Gestalt, ConfigReloadListener {

private final MetricsManager metricsManager;

private final ValidationManager validationManager;

private final DecoderContext decoderContext;

/**
Expand All @@ -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<ConfigSourcePackage> configSourcePackages,
DecoderService decoderService, SentenceLexer sentenceLexer, GestaltConfig gestaltConfig,
ConfigNodeService configNodeService, CoreReloadListenersContainer reloadStrategy,
List<PostProcessor> postProcessor, SecretConcealer secretConcealer,
MetricsManager metricsManager, Tags defaultTags) {
MetricsManager metricsManager, ValidationManager validationManager, Tags defaultTags) {
this.configLoaderService = configLoaderService;
this.sourcePackages = configSourcePackages;
this.decoderService = decoderService;
Expand All @@ -94,6 +99,7 @@ public GestaltCore(ConfigLoaderService configLoaderService, List<ConfigSourcePac
this.postProcessors = postProcessor != null ? postProcessor : Collections.emptyList();
this.secretConcealer = secretConcealer;
this.metricsManager = metricsManager;
this.validationManager = validationManager;
this.defaultTags = defaultTags;
this.decoderContext = new DecoderContext(decoderService, this, secretConcealer);
}
Expand Down Expand Up @@ -292,7 +298,7 @@ public <T> T getConfig(String path, TypeCapture<T> klass, Tags tags) throws Gest
// most likely an optional.empty()
Pair<Boolean, T> isOptionalAndDefault = ClassUtils.isOptionalAndDefault(klass.getRawType());

return getConfigInternal(path, !isOptionalAndDefault.getFirst(), isOptionalAndDefault.getSecond(), klass, tags);
return getConfigurationInternal(path, !isOptionalAndDefault.getFirst(), isOptionalAndDefault.getSecond(), klass, tags);
}

@Override
Expand Down Expand Up @@ -323,7 +329,7 @@ public <T> T getConfig(String path, T defaultVal, TypeCapture<T> 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());
}
Expand Down Expand Up @@ -359,7 +365,7 @@ public <T> Optional<T> getConfigOptional(String path, TypeCapture<T> 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());
Expand All @@ -368,13 +374,15 @@ public <T> Optional<T> getConfigOptional(String path, TypeCapture<T> klass, Tags
return Optional.empty();
}

private <T> T getConfigInternal(String path, boolean failOnErrors, T defaultVal, TypeCapture<T> klass, Tags tags)
private <T> T getConfigurationInternal(String path, boolean failOnErrors, T defaultVal, TypeCapture<T> 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);
Expand All @@ -385,7 +393,7 @@ private <T> T getConfigInternal(String path, boolean failOnErrors, T defaultVal,
if (tokens.hasErrors()) {
throw new GestaltException("Unable to parse path: " + combinedPath, tokens.getErrors());
} else {
GResultOf<T> results = getConfigInternal(combinedPath, tokens.results(), klass, tags);
GResultOf<T> results = getAndDecodeConfig2(combinedPath, tokens.results(), klass, tags);

getConfigMetrics(results);

Expand All @@ -399,9 +407,7 @@ private <T> 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;
}
Expand All @@ -413,11 +419,35 @@ private <T> 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;
}
}

Expand All @@ -430,22 +460,33 @@ private <T> 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<Tag> 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 <T> GResultOf<T> getConfigInternal(String path, List<Token> tokens, TypeCapture<T> klass, Tags tags) {
private <T> GResultOf<T> getAndDecodeConfig2(String path, List<Token> tokens, TypeCapture<T> klass, Tags tags) {
GResultOf<ConfigNode> node = configNodeService.navigateToNode(path, tokens, tags);

if (!node.hasErrors() || node.hasErrors(ValidationLevel.MISSING_VALUE)) {
Expand Down Expand Up @@ -507,6 +548,15 @@ private <T> void getConfigMetrics(GResultOf<T> results) throws GestaltException
}
}

private <T> void updateValidationMetrics(GResultOf<T> 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;
Expand All @@ -522,6 +572,10 @@ private boolean ignoreError(ValidationError error) {
return error.level() == ValidationLevel.WARN || error.level() == ValidationLevel.DEBUG;
}

private <T> boolean shouldValidate(TypeCapture<T> klass) {
return !klass.isAssignableFrom(String.class) && !ClassUtils.isPrimitiveOrWrapper(klass.getRawType());
}

/**
* Prints out the contents of a config root for the tag.
*
Expand Down
Loading

0 comments on commit a402730

Please sign in to comment.