Skip to content

Commit

Permalink
feat: Add configuration option to allow empty collections to be retur…
Browse files Browse the repository at this point in the history
…ned instead of errors. #127
  • Loading branch information
credmond-git committed Dec 8, 2023
1 parent f7ec300 commit 6b4cc54
Show file tree
Hide file tree
Showing 13 changed files with 526 additions and 28 deletions.
30 changes: 16 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,8 @@ Using the extension functions you don't need to specify the type if the return t
val hosts: List<Host> = gestalt.getConfig("db.hosts", emptyList())
```
| Gestalt Version | Kotlin Version |
|------------------|----------------|
|------------------|----------------|
| 0.25.0 + | 1.9 |
| 0.17.0 + | 1.8 |
| 0.13.0 to 0.16.6 | 1.7 |
| 0.10.0 to 0.12.0 | 1.6 |
Expand Down Expand Up @@ -807,19 +808,20 @@ Once Gestalt has reloaded the config it will send out its own Gestalt Core Reloa
| TimedConfigReloadStrategy | Provide a ConfigSource and a Duration then the Reload Strategy will reload every period defined by the Duration |

# Gestalt configuration
| Configuration | default | Details |
|-------------------------------|----------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| treatWarningsAsErrors | false | if we treat warnings as errors Gestalt will fail on any warnings. When set to true it overrides the behaviour in the below configs. |
| treatMissingArrayIndexAsError | false | By default Gestalt will insert null values into an array or list that is missing an index. By enabling this you will get an exception instead |
| treatMissingValuesAsErrors | false | By default Gestalt will not update values in classes not found in the config. Null values will be left null and values with defaults will keep their defaults. By enabling this you will get an exception if any value is missing. |
| treatNullValuesInClassAsErrors | true | Prior to v0.20.0 null values and values not in the config but have a default in classes were treated the same. By enabling this you will get an exception if a value is null after decoding an object. If the value is missing but has a default this will be caught under the config treatMissingValuesAsErrors |
| dateDecoderFormat | null | Pattern for a DateTimeFormatter, if left blank will use the default for the decoder |
| localDateTimeFormat | null | Pattern for a DateTimeFormatter, if left blank will use the default for the decoder |
| localDateFormat | null | Pattern for a DateTimeFormatter, if left blank will use the default for the decoder |
| substitutionOpeningToken | ${ | Customize what tokens gestalt looks for when starting replacing substrings |
| substitutionClosingToken | } | Customize what tokens gestalt looks for when ending replacing substrings |
| maxSubstitutionNestedDepth | 5 | Get the maximum string substitution nested depth. If you have nested or recursive substitutions that go deeper than this it will fail. |
| proxyDecoderMode | CACHE | Either CACHE or PASSTHROUGH, where cache means we serve results through a cache that is never updated or pass through where each call is forwarded to Gestalt to be looked up. |
| Configuration | default | Details |
|--------------------------------|--------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| treatWarningsAsErrors | false | if we treat warnings as errors Gestalt will fail on any warnings. When set to true it overrides the behaviour in the below configs. |
| treatMissingArrayIndexAsError | false | By default Gestalt will insert null values into an array or list that is missing an index. By enabling this you will get an exception instead |
| treatMissingValuesAsErrors | false | By default Gestalt will not update values in classes not found in the config. Null values will be left null and values with defaults will keep their defaults. By enabling this you will get an exception if any value is missing. |
| treatNullValuesInClassAsErrors | true | Prior to v0.20.0 null values and values not in the config but have a default in classes were treated the same. By enabling this you will get an exception if a value is null after decoding an object. If the value is missing but has a default this will be caught under the config treatMissingValuesAsErrors |
| treatEmptyCollectionAsErrors | true | By default Gestalt will treat empty (null, size 0) collections as an error. By setting this to false, if there is an empty collection it will simply return an empty collection |
| dateDecoderFormat | null | Pattern for a DateTimeFormatter, if left blank will use the default for the decoder |
| localDateTimeFormat | null | Pattern for a DateTimeFormatter, if left blank will use the default for the decoder |
| localDateFormat | null | Pattern for a DateTimeFormatter, if left blank will use the default for the decoder |
| substitutionOpeningToken | ${ | Customize what tokens gestalt looks for when starting replacing substrings |
| substitutionClosingToken | } | Customize what tokens gestalt looks for when ending replacing substrings |
| maxSubstitutionNestedDepth | 5 | Get the maximum string substitution nested depth. If you have nested or recursive substitutions that go deeper than this it will fail. |
| proxyDecoderMode | CACHE | Either CACHE or PASSTHROUGH, where cache means we serve results through a cache that is never updated or pass through where each call is forwarded to Gestalt to be looked up. |

# Logging
Gestalt leverages [System.logger](https://docs.oracle.com/javase/9/docs/api/java/lang/System.Logger.html), the jdk logging library to provide a logging facade. Many logging libraries provide backends for System Logger.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ public class GestaltBuilder {
private Boolean treatMissingArrayIndexAsError = null;
private Boolean treatMissingValuesAsErrors = null;
private Boolean treatNullValuesInClassAsErrors = null;
private Boolean treatEmptyCollectionAsErrors = null;

private Level logLevelForMissingValuesWhenDefaultOrOptional = null;

Expand Down Expand Up @@ -575,6 +576,26 @@ public GestaltBuilder setTreatNullValuesInClassAsErrors(Boolean treatNullValuesI
return this;
}

/**
* Set treat empty collections or arrays as errors.
*
* @param treatEmptyCollectionAsErrors Treat empty collections or arrays as errors.
* @return GestaltBuilder builder
*/
public GestaltBuilder setTreatEmptyCollectionAsErrors(Boolean treatEmptyCollectionAsErrors) {
this.treatEmptyCollectionAsErrors = treatEmptyCollectionAsErrors;
return this;
}

/**
* Get treat empty collections or arrays as errors.
*
* @return Treat empty collections or arrays as errors.
*/
public Boolean isTreatEmptyCollectionAsErrors() {
return treatEmptyCollectionAsErrors;
}

/**
* Add a cache layer to gestalt.
*
Expand Down Expand Up @@ -889,6 +910,9 @@ private GestaltConfig rebuildConfig() {
newConfig.setTreatNullValuesInClassAsErrors(Objects.requireNonNullElseGet(treatNullValuesInClassAsErrors,
() -> gestaltConfig.isTreatNullValuesInClassAsErrors()));

newConfig.setTreatEmptyCollectionAsErrors(Objects.requireNonNullElseGet(treatEmptyCollectionAsErrors,
() -> gestaltConfig.isTreatEmptyCollectionAsErrors()));

newConfig.setLogLevelForMissingValuesWhenDefaultOrOptional(
Objects.requireNonNullElseGet(logLevelForMissingValuesWhenDefaultOrOptional,
() -> gestaltConfig.getLogLevelForMissingValuesWhenDefaultOrOptional()));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.github.gestalt.config.decoder;

import org.github.gestalt.config.entity.GestaltConfig;
import org.github.gestalt.config.entity.ValidationError;
import org.github.gestalt.config.node.ArrayNode;
import org.github.gestalt.config.node.ConfigNode;
Expand All @@ -23,11 +24,18 @@
*/
public final class ArrayDecoder<T> implements Decoder<T[]> {

private boolean treatEmptyCollectionAsErrors = true;

@Override
public Priority priority() {
return Priority.MEDIUM;
}

@Override
public void applyConfig(GestaltConfig config) {
this.treatEmptyCollectionAsErrors = config.isTreatEmptyCollectionAsErrors();
}

@Override
public String name() {
return "Array";
Expand All @@ -42,7 +50,7 @@ public boolean canDecode(String path, Tags tags, ConfigNode node, TypeCapture<?>
public ValidateOf<T[]> decode(String path, Tags tags, ConfigNode node, TypeCapture<?> type, DecoderContext decoderContext) {
ValidateOf<T[]> results;
if (node instanceof ArrayNode) {
if (node.size() > 0) {
if (node.size() > 0 || !treatEmptyCollectionAsErrors) {
results = arrayDecode(path, tags, node, type, decoderContext);
} else {
results = ValidateOf.inValid(new ValidationError.DecodingArrayMissingValue(path, name()));
Expand All @@ -58,9 +66,13 @@ public ValidateOf<T[]> decode(String path, Tags tags, ConfigNode node, TypeCaptu
.collect(Collectors.toList());

results = arrayDecode(path, tags, new ArrayNode(leafNodes), type, decoderContext);
} else if (!treatEmptyCollectionAsErrors) {
results = arrayDecode(path, tags, new ArrayNode(List.of()), type, decoderContext);
} else {
results = ValidateOf.inValid(new ValidationError.DecodingLeafMissingValue(path, name()));
}
} else if (!treatEmptyCollectionAsErrors) {
results = arrayDecode(path, tags, new ArrayNode(List.of()), type, decoderContext);
} else {
results = ValidateOf.inValid(new ValidationError.DecodingExpectedArrayNodeType(path, node, name()));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.github.gestalt.config.decoder;

import org.github.gestalt.config.entity.GestaltConfig;
import org.github.gestalt.config.entity.ValidationError;
import org.github.gestalt.config.node.ArrayNode;
import org.github.gestalt.config.node.ConfigNode;
Expand All @@ -21,16 +22,23 @@
*/
public abstract class CollectionDecoder<T extends Collection<?>> implements Decoder<T> {

protected boolean treatEmptyCollectionAsErrors = true;

@Override
public Priority priority() {
return Priority.MEDIUM;
}

@Override
public void applyConfig(GestaltConfig config) {
this.treatEmptyCollectionAsErrors = config.isTreatEmptyCollectionAsErrors();
}

@Override
public ValidateOf<T> decode(String path, Tags tags, ConfigNode node, TypeCapture<?> type, DecoderContext decoderContext) {
ValidateOf<T> results;
if (node instanceof ArrayNode) {
if (node.size() > 0) {
if (node.size() > 0 || !treatEmptyCollectionAsErrors) {
results = arrayDecode(path, tags, node, type, decoderContext);
} else {
results = ValidateOf.inValid(new ValidationError.DecodingArrayMissingValue(path, name()));
Expand All @@ -40,15 +48,19 @@ public ValidateOf<T> decode(String path, Tags tags, ConfigNode node, TypeCapture
String value = node.getValue().get();
String[] array = value.split("(?<!\\\\),");
List<ConfigNode> leafNodes = Arrays.stream(array)
.map(String::trim)
.map(it -> it.replace("\\,", ","))
.map(LeafNode::new)
.collect(Collectors.toList());
.map(String::trim)
.map(it -> it.replace("\\,", ","))
.map(LeafNode::new)
.collect(Collectors.toList());

results = arrayDecode(path, tags, new ArrayNode(leafNodes), type, decoderContext);
} else if (!treatEmptyCollectionAsErrors) {
results = arrayDecode(path, tags, new ArrayNode(List.of()), type, decoderContext);
} else {
results = ValidateOf.inValid(new ValidationError.DecodingLeafMissingValue(path, name()));
}
} else if (!treatEmptyCollectionAsErrors) {
results = arrayDecode(path, tags, new ArrayNode(List.of()), type, decoderContext);
} else {
results = ValidateOf.inValid(new ValidationError.DecodingExpectedArrayNodeType(path, node, name()));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ protected ValidateOf<List<?>> arrayDecode(String path, Tags tags, ConfigNode nod
}
}

var validResults = !results.isEmpty() || !treatEmptyCollectionAsErrors ? results : null;

return ValidateOf.validateOf(!results.isEmpty() ? results : null, errors);
return ValidateOf.validateOf(validResults, errors);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@ protected ValidateOf<Set<?>> arrayDecode(String path, Tags tags, ConfigNode node
}
}

var validResults = !results.isEmpty() || !treatEmptyCollectionAsErrors ? results : null;

return ValidateOf.validateOf(!results.isEmpty() ? results : null, errors);
return ValidateOf.validateOf(validResults, errors);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ public class GestaltConfig {
private boolean treatMissingValuesAsErrors = false;
// Treat null values in classes after decoding as errors.
private boolean treatNullValuesInClassAsErrors = true;
// Treat empty collections or arrays as errors.
private boolean treatEmptyCollectionAsErrors = true;
// For the proxy decoder, if we should use a cached value or call gestalt for the most recent value.
private ProxyDecoderMode proxyDecoderMode = ProxyDecoderMode.CACHE;
// Provide the log level when we log a message when a config is missing, but we provided a default, or it is Optional.
Expand Down Expand Up @@ -115,6 +117,24 @@ public void setTreatNullValuesInClassAsErrors(boolean treatNullValuesInClassAsEr
this.treatNullValuesInClassAsErrors = treatNullValuesInClassAsErrors;
}

/**
* Treat empty collections or arrays as errors.
*
* @return Treat empty collections or arrays as errors.
*/
public boolean isTreatEmptyCollectionAsErrors() {
return treatEmptyCollectionAsErrors;
}

/**
* Set treat empty collections or arrays as errors.
*
* @param treatEmptyCollectionAsErrors Treat empty collections or arrays as errors.
*/
public void setTreatEmptyCollectionAsErrors(boolean treatEmptyCollectionAsErrors) {
this.treatEmptyCollectionAsErrors = treatEmptyCollectionAsErrors;
}

/**
* Get For the proxy decoder mode, if we should use a cached value or call gestalt for the most recent value.
*
Expand Down
Loading

0 comments on commit 6b4cc54

Please sign in to comment.