Skip to content

Commit

Permalink
test: add more unit tests.
Browse files Browse the repository at this point in the history
  • Loading branch information
credmond-git committed Apr 24, 2024
1 parent 282714f commit b121f68
Show file tree
Hide file tree
Showing 10 changed files with 304 additions and 25 deletions.
83 changes: 83 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1603,3 +1603,86 @@ To register your own default Transformer, add it to a file in `META-INF\services
the annotation `@ConfigPriority(100)`, specifies the descending priority order to check your transformer when a substitution has been made without specifying the source ${key}


# Result Processors

Result Processors are used to modify the result of getting a configuration and decoding it.
Each processor has an annotation `@ConfigPriority` so we run them in order passing the output of one Result Processor as the input to the next.

Gestalt has two core result processors `ErrorResultProcessor` and `DefaultResultProcessor`.
The `ErrorResultProcessor` throws a `GestaltException` if there is an unrecoverable error.
The `DefaultResultProcessor` will convert the result into a default value if there is no result.

To implement your own Result Processors you need to inherit from ResultProcessor.

To automatically register your own default `ResultProcessor`, add it to a file in `META-INF\services\org.github.gestalt.config.processor.result.ResultProcessor` and add the full package of classpath your `ResultProcessor`.

Alternatively, you can implement the interface and register it with the gestalt builder `addResultProcessors(List<ResultProcessor> resultProcessorSet)`.

```java
public interface ResultProcessor {

/**
* If your Result Processor needs access to the Gestalt Config.
*
* @param config Gestalt configuration
*/
default void applyConfig(GestaltConfig config) {}

/**
* Returns the {@link GResultOf} with any processed results.
* You can modify the results, errors or any combination.
* If your post processor does nothing to the node, return the original node.
*
* @param results GResultOf to process.
* @param path path the object was located at
* @param isOptional if the result is optional (an Optional or has a default.
* @param defaultVal value to return in the event of failure.
* @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 <T> Class of the object.
* @throws GestaltException for any exceptions while processing the results, such as if there are errors in the result.
*/
<T> GResultOf<T> processResults(GResultOf<T> results, String path, boolean isOptional,
T defaultVal, TypeCapture<T> klass, Tags tags)
throws GestaltException;
}
```

## Validation
Validations are implemented as a Result Processor as well in `ValidationResultProcessor`.
To ensure a simple API for validations it does not use the ResultProcessor interface but a `ConfigValidator` interface.

To automatically register your own default `ConfigValidator`, add it to a file in `META-INF\services\org.github.gestalt.config.processor.result.validation.ConfigValidator` and add the full package of classpath `ConfigValidator`. This is how `gestalt-validator-hibernate` automatically is discovered.

Alternatively, you can implement the interface and register it with the gestalt builder `addValidators(List<ConfigValidator> validatorsSet)`.

```java
/**
* Interface for validating objects.
*
* @author <a href="mailto:colin.redmond@outlook.com"> Colin Redmond </a> (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 <T> Class of the object.
*/
<T> GResultOf<T> validator(T obj, String path, TypeCapture<T> klass, Tags tags);
}
```
2 changes: 1 addition & 1 deletion gestalt-core/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,10 @@
exports org.github.gestalt.config.tag;
exports org.github.gestalt.config.token;
exports org.github.gestalt.config.utils;
exports org.github.gestalt.config.processor.result;
exports org.github.gestalt.config.processor.config;
exports org.github.gestalt.config.processor.config.transform;
exports org.github.gestalt.config.processor.config.transform.substitution;
exports org.github.gestalt.config.processor.result;
exports org.github.gestalt.config.processor.result.validation;

provides org.github.gestalt.config.decoder.Decoder with
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -503,12 +503,9 @@ public GestaltBuilder addPathMappers(List<PathMapper> pathMappers) throws Gestal
*
* @param observationRecorders list of observationRecorders to record observations to.
* @return GestaltBuilder builder
* @throws GestaltConfigurationException exception if there are no ObservationRecorder
*/
public GestaltBuilder setObservationsRecorders(List<ObservationRecorder> observationRecorders) throws GestaltConfigurationException {
if (observationRecorders == null || observationRecorders.isEmpty()) {
throw new GestaltConfigurationException("No ObservationRecorder provided while setting");
}
public GestaltBuilder setObservationsRecorders(List<ObservationRecorder> observationRecorders) {
Objects.requireNonNull(observationRecorders, "No ObservationRecorder provided while setting");
this.observationRecorders = observationRecorders;

return this;
Expand All @@ -519,9 +516,8 @@ public GestaltBuilder setObservationsRecorders(List<ObservationRecorder> observa
*
* @param observationRecordersSet list of ObservationRecorder to add.
* @return GestaltBuilder builder
* @throws GestaltConfigurationException no ObservationRecorder provided
*/
public GestaltBuilder addObservationsRecorders(List<ObservationRecorder> observationRecordersSet) throws GestaltConfigurationException {
public GestaltBuilder addObservationsRecorders(List<ObservationRecorder> observationRecordersSet) {
Objects.requireNonNull(observationRecordersSet, "ObservationRecorder should not be null");

observationRecorders.addAll(observationRecordersSet);
Expand All @@ -546,12 +542,9 @@ public GestaltBuilder addObservationsRecorder(ObservationRecorder observationRec
*
* @param configValidators list of Validator to validate objects.
* @return GestaltBuilder builder
* @throws GestaltConfigurationException exception if there are no Validator
*/
public GestaltBuilder setValidators(List<ConfigValidator> configValidators) throws GestaltConfigurationException {
if (configValidators == null || configValidators.isEmpty()) {
throw new GestaltConfigurationException("No Validators provided while setting");
}
public GestaltBuilder setValidators(List<ConfigValidator> configValidators) {
Objects.requireNonNull(configValidators, "No Validators provided while setting");
this.configValidators = configValidators;

return this;
Expand Down Expand Up @@ -588,12 +581,9 @@ public GestaltBuilder addValidator(ConfigValidator configValidator) {
*
* @param resultProcessors list of resultProcessors.
* @return GestaltBuilder builder
* @throws GestaltConfigurationException exception if there are no ResultProcessor
*/
public GestaltBuilder setResultProcessor(List<ResultProcessor> resultProcessors) throws GestaltConfigurationException {
if (resultProcessors == null || resultProcessors.isEmpty()) {
throw new GestaltConfigurationException("No ResultProcessor provided while setting");
}
public GestaltBuilder setResultProcessor(List<ResultProcessor> resultProcessors) {
Objects.requireNonNull(resultProcessors, "ResultProcessor should not be null");
this.resultProcessors = resultProcessors;

return this;
Expand All @@ -604,9 +594,8 @@ public GestaltBuilder setResultProcessor(List<ResultProcessor> resultProcessors)
*
* @param resultProcessorSet list of ResultProcessor to add.
* @return GestaltBuilder builder
* @throws GestaltConfigurationException no ResultProcessor provided
*/
public GestaltBuilder addResultProcessors(List<ResultProcessor> resultProcessorSet) throws GestaltConfigurationException {
public GestaltBuilder addResultProcessors(List<ResultProcessor> resultProcessorSet) {
Objects.requireNonNull(resultProcessorSet, "ResultProcessor should not be null");

resultProcessors.addAll(resultProcessorSet);
Expand Down Expand Up @@ -939,9 +928,11 @@ public GestaltBuilder setValidationEnabled(Boolean validationEnabled) {
* If they are not added you will get strange and possible incorrect behaviour.
*
* @param addCoreResultProcessors add a core result processor.
* @return GestaltBuilder builder
*/
public void setAddCoreResultProcessors(boolean addCoreResultProcessors) {
public GestaltBuilder setAddCoreResultProcessors(boolean addCoreResultProcessors) {
this.addCoreResultProcessors = addCoreResultProcessors;
return this;
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
public interface ResultProcessor {

/**
* If your Validator needs access to the Gestalt Config.
* If your Result Processor needs access to the Gestalt Config.
*
* @param config Gestalt configuration
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import org.github.gestalt.config.utils.ClassUtils;
import org.github.gestalt.config.utils.GResultOf;

import java.util.ArrayList;
import java.util.List;
import java.util.ServiceLoader;
import java.util.stream.Collectors;
Expand All @@ -24,11 +25,12 @@ public class ValidationResultProcessor implements ResultProcessor {

private GestaltConfig gestaltConfig;
private ObservationManager observationManager;
private List<ConfigValidator> configValidators;
private final List<ConfigValidator> configValidators;

public ValidationResultProcessor() {
configValidators = new ArrayList<>();
ServiceLoader<ConfigValidator> loader = ServiceLoader.load(ConfigValidator.class);
loader.forEach(it -> configValidators.add(it));
loader.forEach(configValidators::add);
}

public ValidationResultProcessor(List<ConfigValidator> configValidators, ObservationManager observationManager) {
Expand Down Expand Up @@ -80,7 +82,7 @@ private <T> boolean shouldValidate(TypeCapture<T> klass) {
}

private <T> void updateValidationObservations(List<ValidationError> errors) {
if (gestaltConfig != null && gestaltConfig.isObservationsEnabled() && observationManager != null && !errors.isEmpty()) {
if (gestaltConfig != null && gestaltConfig.isObservationsEnabled() && observationManager != null) {
observationManager.recordObservation("get.config.validation.error", errors.size(), Tags.of());
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@
import org.github.gestalt.config.path.mapper.DotNotationPathMapper;
import org.github.gestalt.config.path.mapper.PathMapper;
import org.github.gestalt.config.path.mapper.StandardPathMapper;
import org.github.gestalt.config.processor.TestValidationProcessor;
import org.github.gestalt.config.processor.config.ConfigNodeProcessor;
import org.github.gestalt.config.processor.config.ConfigNodeProcessorConfig;
import org.github.gestalt.config.processor.config.transform.EnvironmentVariablesTransformer;
import org.github.gestalt.config.processor.config.transform.StringSubstitutionConfigNodeProcessor;
import org.github.gestalt.config.processor.result.DefaultResultProcessor;
import org.github.gestalt.config.processor.result.ErrorResultProcessor;
import org.github.gestalt.config.reflect.TypeCapture;
import org.github.gestalt.config.reload.TimedConfigReloadStrategy;
import org.github.gestalt.config.source.ConfigSource;
Expand Down Expand Up @@ -120,6 +123,10 @@ public void build() throws GestaltException {
.addResultProcessors(List.of(new TestResultProcessor(true)))
.setResultProcessor(List.of(new TestResultProcessor(true)))
.setResultsProcessorManager(new ResultsProcessorManager(new ArrayList<>()))
.setAddCoreResultProcessors(true)
.addValidator(new TestValidationProcessor(true))
.addValidators(List.of(new TestValidationProcessor(true)))
.setValidators(List.of(new TestValidationProcessor(true)))
.setSecurityMaskingRule(new HashSet<>())
.addSecurityMaskingRule("secret")
.setSecurityMask("&&&&")
Expand Down Expand Up @@ -822,6 +829,68 @@ public void manuallyAddedPathMapperConfig() throws GestaltException {
Assertions.assertEquals(1, mapper3.configCount);
}

@Test
public void buildWithResultProcessorManager() throws GestaltException {
Map<String, String> configs = new HashMap<>();
configs.put("db.name", "test");
configs.put("db.port", "3306");
configs.put("admin[0]", "John");
configs.put("admin[1]", "Steve");

Map<String, String> configs2 = new HashMap<>();
configs2.put("db.name", "test2");
configs2.put("db.password", "pass");
configs2.put("admin[0]", "John2");
configs2.put("admin[1]", "Steve2");

List<ConfigSourcePackage> sources = new ArrayList<>();
sources.add(MapConfigSourceBuilder.builder().setCustomConfig(configs).build());
sources.add(MapConfigSourceBuilder.builder().setCustomConfig(configs2).build());

GestaltBuilder builder = new GestaltBuilder();
Gestalt gestalt = builder
.addSources(sources)
.setResultsProcessorManager(new ResultsProcessorManager(List.of(new ErrorResultProcessor(), new DefaultResultProcessor())))
.build();

gestalt.loadConfigs();
Assertions.assertEquals("pass", gestalt.getConfig("db.password", String.class));
Assertions.assertEquals("test2", gestalt.getConfig("db.name", String.class));
Assertions.assertEquals("3306", gestalt.getConfig("db.port", String.class));
}

@Test
public void buildWithNoResultProcessorManager() throws GestaltException {
Map<String, String> configs = new HashMap<>();
configs.put("db.name", "test");
configs.put("db.port", "3306");
configs.put("admin[0]", "John");
configs.put("admin[1]", "Steve");

Map<String, String> configs2 = new HashMap<>();
configs2.put("db.name", "test2");
configs2.put("db.password", "pass");
configs2.put("admin[0]", "John2");
configs2.put("admin[1]", "Steve2");

List<ConfigSourcePackage> sources = new ArrayList<>();
sources.add(MapConfigSourceBuilder.builder().setCustomConfig(configs).build());
sources.add(MapConfigSourceBuilder.builder().setCustomConfig(configs2).build());

GestaltBuilder builder = new GestaltBuilder();
Gestalt gestalt = builder
.addSources(sources)
.setAddCoreResultProcessors(false)
.useCacheDecorator(false)
.build();

gestalt.loadConfigs();
Assertions.assertEquals("pass", gestalt.getConfig("db.password", String.class));
Assertions.assertEquals("test2", gestalt.getConfig("db.name", String.class));
Assertions.assertEquals("3306", gestalt.getConfig("db.port", String.class));
Assertions.assertNull(gestalt.getConfig("db.none", "default", String.class));
}

private static class TestDecoder implements Decoder {

public int configCount = 0;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,26 @@ public String description() {
Assertions.assertEquals("something broke", results.getErrors().get(0).description());
}

@Test
public void testTwoResultProcessorResultHasError2() throws GestaltException {
var validationManager = new ResultsProcessorManager(List.of(
new TestResultProcessor(false), new TestResultProcessor(false), new TestResultProcessor3(false)));

var results = validationManager.processResults(GResultOf.errors(new ValidationError(ValidationLevel.ERROR) {
@Override
public String description() {
return "there was an error";
}
}), "my.path", false, null,
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());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package org.github.gestalt.config.processor;

import org.github.gestalt.config.entity.ValidationError;
import org.github.gestalt.config.entity.ValidationLevel;
import org.github.gestalt.config.processor.result.ResultProcessor;
import org.github.gestalt.config.reflect.TypeCapture;
import org.github.gestalt.config.tag.Tags;
import org.github.gestalt.config.utils.GResultOf;

public class TestResultProcessor3 implements ResultProcessor {

public boolean isOk = true;

public TestResultProcessor3(boolean isOk) {
this.isOk = isOk;
}

@Override
public <T> GResultOf<T> processResults(GResultOf<T> results, String path, boolean isOptional, T defaultVal,
TypeCapture<T> klass, Tags tags) {
if (isOk) {
return results;
} else {
return GResultOf.errors(new ValidationError(ValidationLevel.ERROR) {
@Override
public String description() {
return "something broke";
}
});
}
}
}
Loading

0 comments on commit b121f68

Please sign in to comment.