From 6b4cc54ae5e82e467f64a8c27964bfe7b6a49a4b Mon Sep 17 00:00:00 2001 From: Colin Redmond Date: Thu, 7 Dec 2023 23:02:24 -0800 Subject: [PATCH] feat: Add configuration option to allow empty collections to be returned instead of errors. https://github.com/gestalt-config/gestalt/issues/127 --- README.md | 30 ++--- .../config/builder/GestaltBuilder.java | 24 ++++ .../gestalt/config/decoder/ArrayDecoder.java | 14 ++- .../config/decoder/CollectionDecoder.java | 22 +++- .../gestalt/config/decoder/ListDecoder.java | 3 +- .../gestalt/config/decoder/SetDecoder.java | 3 +- .../gestalt/config/entity/GestaltConfig.java | 20 ++++ .../config/builder/GestaltBuilderTest.java | 2 + .../config/decoder/ArrayDecoderTest.java | 96 ++++++++++++++- .../config/decoder/ListDecoderTest.java | 86 ++++++++++++++ .../config/decoder/SetDecoderTest.java | 87 ++++++++++++++ .../integration/GestaltIntegrationTests.java | 111 ++++++++++++++++++ .../config/integration/GestaltSample.java | 56 +++++++++ 13 files changed, 526 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 97b7e962a..3dfc9b1e2 100644 --- a/README.md +++ b/README.md @@ -503,7 +503,8 @@ Using the extension functions you don't need to specify the type if the return t val hosts: List = 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 | @@ -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. 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 bb15e516d..a18e61cc3 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 @@ -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; @@ -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. * @@ -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())); diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ArrayDecoder.java b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ArrayDecoder.java index b4da66baa..1e056691d 100644 --- a/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ArrayDecoder.java +++ b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ArrayDecoder.java @@ -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; @@ -23,11 +24,18 @@ */ public final class ArrayDecoder implements Decoder { + 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"; @@ -42,7 +50,7 @@ public boolean canDecode(String path, Tags tags, ConfigNode node, TypeCapture public ValidateOf decode(String path, Tags tags, ConfigNode node, TypeCapture type, DecoderContext decoderContext) { ValidateOf 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())); @@ -58,9 +66,13 @@ public ValidateOf 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())); } diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/decoder/CollectionDecoder.java b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/CollectionDecoder.java index b4687198e..d69c9d20b 100644 --- a/gestalt-core/src/main/java/org/github/gestalt/config/decoder/CollectionDecoder.java +++ b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/CollectionDecoder.java @@ -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; @@ -21,16 +22,23 @@ */ public abstract class CollectionDecoder> implements Decoder { + protected boolean treatEmptyCollectionAsErrors = true; + @Override public Priority priority() { return Priority.MEDIUM; } + @Override + public void applyConfig(GestaltConfig config) { + this.treatEmptyCollectionAsErrors = config.isTreatEmptyCollectionAsErrors(); + } + @Override public ValidateOf decode(String path, Tags tags, ConfigNode node, TypeCapture type, DecoderContext decoderContext) { ValidateOf 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())); @@ -40,15 +48,19 @@ public ValidateOf decode(String path, Tags tags, ConfigNode node, TypeCapture String value = node.getValue().get(); String[] array = value.split("(? 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())); } diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ListDecoder.java b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ListDecoder.java index 26d355303..bdc0e865e 100644 --- a/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ListDecoder.java +++ b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/ListDecoder.java @@ -51,7 +51,8 @@ protected ValidateOf> 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); } } diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/decoder/SetDecoder.java b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/SetDecoder.java index bdc43b3d1..70fa0df4d 100644 --- a/gestalt-core/src/main/java/org/github/gestalt/config/decoder/SetDecoder.java +++ b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/SetDecoder.java @@ -51,7 +51,8 @@ protected ValidateOf> 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); } } 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 b668618a1..5c84df07f 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 @@ -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. @@ -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. * 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 d3a4c77b4..7582dc49a 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 @@ -71,6 +71,7 @@ public void build() throws GestaltException { .setDecoders(decoders) .addDecoder(new LongDecoder()) .setTreatWarningsAsErrors(true) + .setTreatEmptyCollectionAsErrors(true) .setLogLevelForMissingValuesWhenDefaultOrOptional(System.Logger.Level.DEBUG) .setSubstitutionOpeningToken("${") .setSubstitutionClosingToken("}") @@ -97,6 +98,7 @@ public void build() throws GestaltException { Assertions.assertEquals(5, builder.getMaxSubstitutionNestedDepth()); Assertions.assertEquals(true, builder.isTreatWarningsAsErrors()); + Assertions.assertEquals(true, builder.isTreatEmptyCollectionAsErrors()); Assertions.assertEquals("", builder.getSubstitutionRegex()); Assertions.assertEquals(ProxyDecoderMode.CACHE, builder.getProxyDecoderMode()); Assertions.assertEquals(System.Logger.Level.DEBUG, builder.getLogLevelForMissingValuesWhenDefaultOrOptional()); diff --git a/gestalt-core/src/test/java/org/github/gestalt/config/decoder/ArrayDecoderTest.java b/gestalt-core/src/test/java/org/github/gestalt/config/decoder/ArrayDecoderTest.java index e2f76de78..9557c0e37 100644 --- a/gestalt-core/src/test/java/org/github/gestalt/config/decoder/ArrayDecoderTest.java +++ b/gestalt-core/src/test/java/org/github/gestalt/config/decoder/ArrayDecoderTest.java @@ -1,5 +1,6 @@ package org.github.gestalt.config.decoder; +import org.github.gestalt.config.entity.GestaltConfig; import org.github.gestalt.config.exceptions.GestaltConfigurationException; import org.github.gestalt.config.lexer.SentenceLexer; import org.github.gestalt.config.node.*; @@ -18,6 +19,7 @@ import java.util.List; import java.util.Set; +@SuppressWarnings({"rawtypes", "unchecked"}) class ArrayDecoderTest { final DoubleDecoder doubleDecoder = new DoubleDecoder(); final StringDecoder stringDecoder = new StringDecoder(); @@ -35,21 +37,18 @@ void setup() throws GestaltConfigurationException { } @Test - @SuppressWarnings({"rawtypes"}) void name() { ArrayDecoder decoder = new ArrayDecoder(); Assertions.assertEquals("Array", decoder.name()); } @Test - @SuppressWarnings({"rawtypes"}) void priority() { ArrayDecoder decoder = new ArrayDecoder(); Assertions.assertEquals(Priority.MEDIUM, decoder.priority()); } @Test - @SuppressWarnings({"rawtypes", "unchecked"}) void canDecode() { ArrayDecoder decoder = new ArrayDecoder(); @@ -83,7 +82,6 @@ void canDecode() { } @Test - @SuppressWarnings({"rawtypes", "unchecked"}) void arrayDecodeStrings() { ConfigNode[] arrayNode = new ConfigNode[3]; @@ -108,7 +106,6 @@ void arrayDecodeStrings() { } @Test - @SuppressWarnings({"rawtypes", "unchecked"}) void arrayDecodeDoubles() { ConfigNode[] arrayNode = new ConfigNode[3]; @@ -134,7 +131,6 @@ void arrayDecodeDoubles() { } @Test - @SuppressWarnings({"rawtypes", "unchecked"}) void arrayDecodeDoublesMissingIndex() { ConfigNode[] arrayNode = new ConfigNode[4]; @@ -194,6 +190,94 @@ void arrayDecodeNullLeaf() { values.getErrors().get(0).description()); } + @Test + void arrayDecodeEmptyArrayNodeOk() { + ConfigNode nodes = new ArrayNode(List.of()); + ArrayDecoder decoder = new ArrayDecoder(); + + var gestaltConfig = new GestaltConfig(); + gestaltConfig.setTreatEmptyCollectionAsErrors(false); + decoder.applyConfig(gestaltConfig); + + ValidateOf values = decoder.decode("db.hosts", Tags.of(), nodes, + TypeCapture.of(String[].class), new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + + String[] results = (String[]) values.results(); + Assertions.assertEquals(0, results.length); + } + + + @Test + void arrayDecodeNullArrayNodeOk() { + ConfigNode nodes = new ArrayNode(null); + ArrayDecoder decoder = new ArrayDecoder(); + + var gestaltConfig = new GestaltConfig(); + gestaltConfig.setTreatEmptyCollectionAsErrors(false); + decoder.applyConfig(gestaltConfig); + + ValidateOf values = decoder.decode("db.hosts", Tags.of(), nodes, TypeCapture.of(String[].class), + new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(0, values.results().length); + } + + @Test + void arrayDecodeEmptyLeafNodeOk() { + ConfigNode nodes = new LeafNode(""); + ArrayDecoder decoder = new ArrayDecoder(); + + var gestaltConfig = new GestaltConfig(); + gestaltConfig.setTreatEmptyCollectionAsErrors(false); + decoder.applyConfig(gestaltConfig); + + ValidateOf values = decoder.decode("db.hosts", Tags.of(), nodes, TypeCapture.of(String[].class), + new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(1, values.results().length); + Assertions.assertEquals("", values.results()[0]); + } + + @Test + void arrayDecodeNullLeafNodeOk() { + ConfigNode nodes = new LeafNode(null); + ArrayDecoder decoder = new ArrayDecoder(); + + var gestaltConfig = new GestaltConfig(); + gestaltConfig.setTreatEmptyCollectionAsErrors(false); + decoder.applyConfig(gestaltConfig); + + ValidateOf values = decoder.decode("db.hosts", Tags.of(), nodes, TypeCapture.of(String[].class), + new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(0, values.results().length); + } + + @Test + void arrayDecodeNullNodeOk() { + ArrayDecoder decoder = new ArrayDecoder(); + + var gestaltConfig = new GestaltConfig(); + gestaltConfig.setTreatEmptyCollectionAsErrors(false); + decoder.applyConfig(gestaltConfig); + + ValidateOf values = decoder.decode("db.hosts", Tags.of(), null, TypeCapture.of(String[].class), + new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(0, values.results().length); + } + @Test void arrayDecodeWrongTypeDoubles() { diff --git a/gestalt-core/src/test/java/org/github/gestalt/config/decoder/ListDecoderTest.java b/gestalt-core/src/test/java/org/github/gestalt/config/decoder/ListDecoderTest.java index 9b36cc7cd..dd6bdc288 100644 --- a/gestalt-core/src/test/java/org/github/gestalt/config/decoder/ListDecoderTest.java +++ b/gestalt-core/src/test/java/org/github/gestalt/config/decoder/ListDecoderTest.java @@ -1,5 +1,6 @@ package org.github.gestalt.config.decoder; +import org.github.gestalt.config.entity.GestaltConfig; import org.github.gestalt.config.exceptions.GestaltConfigurationException; import org.github.gestalt.config.lexer.SentenceLexer; import org.github.gestalt.config.node.*; @@ -198,6 +199,91 @@ void arrayDecodeNullNode() { values.getErrors().get(0).description()); } + @Test + void arrayDecodeEmptyArrayNodeOk() { + ConfigNode nodes = new ArrayNode(List.of()); + ListDecoder decoder = new ListDecoder(); + + var gestaltConfig = new GestaltConfig(); + gestaltConfig.setTreatEmptyCollectionAsErrors(false); + decoder.applyConfig(gestaltConfig); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), nodes, new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(0, values.results().size()); + } + + @Test + void arrayDecodeNullArrayNodeOk() { + ConfigNode nodes = new ArrayNode(null); + ListDecoder decoder = new ListDecoder(); + + var gestaltConfig = new GestaltConfig(); + gestaltConfig.setTreatEmptyCollectionAsErrors(false); + decoder.applyConfig(gestaltConfig); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), nodes, new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(0, values.results().size()); + } + + @Test + void arrayDecodeEmptyLeafNodeOk() { + ConfigNode nodes = new LeafNode(""); + ListDecoder decoder = new ListDecoder(); + + var gestaltConfig = new GestaltConfig(); + gestaltConfig.setTreatEmptyCollectionAsErrors(false); + decoder.applyConfig(gestaltConfig); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), nodes, new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(1, values.results().size()); + Assertions.assertEquals("", values.results().get(0)); + } + + @Test + void arrayDecodeNullLeafNodeOk() { + ConfigNode nodes = new LeafNode(null); + ListDecoder decoder = new ListDecoder(); + + var gestaltConfig = new GestaltConfig(); + gestaltConfig.setTreatEmptyCollectionAsErrors(false); + decoder.applyConfig(gestaltConfig); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), nodes, new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(0, values.results().size()); + } + + @Test + void arrayDecodeNullNodeOk() { + ListDecoder decoder = new ListDecoder(); + + var gestaltConfig = new GestaltConfig(); + gestaltConfig.setTreatEmptyCollectionAsErrors(false); + decoder.applyConfig(gestaltConfig); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), null, new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(0, values.results().size()); + } + @Test void arrayDecodeWrongTypeDoubles() { diff --git a/gestalt-core/src/test/java/org/github/gestalt/config/decoder/SetDecoderTest.java b/gestalt-core/src/test/java/org/github/gestalt/config/decoder/SetDecoderTest.java index a8b069b68..0bb88411e 100644 --- a/gestalt-core/src/test/java/org/github/gestalt/config/decoder/SetDecoderTest.java +++ b/gestalt-core/src/test/java/org/github/gestalt/config/decoder/SetDecoderTest.java @@ -1,5 +1,6 @@ package org.github.gestalt.config.decoder; +import org.github.gestalt.config.entity.GestaltConfig; import org.github.gestalt.config.exceptions.GestaltConfigurationException; import org.github.gestalt.config.lexer.SentenceLexer; import org.github.gestalt.config.node.*; @@ -215,6 +216,92 @@ void arrayDecodeNullNode() { values.getErrors().get(0).description()); } + + @Test + void arrayDecodeEmptyArrayNodeOk() { + ConfigNode nodes = new ArrayNode(List.of()); + SetDecoder decoder = new SetDecoder(); + + var gestaltConfig = new GestaltConfig(); + gestaltConfig.setTreatEmptyCollectionAsErrors(false); + decoder.applyConfig(gestaltConfig); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), nodes, new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(0, values.results().size()); + } + + @Test + void arrayDecodeNullArrayNodeOk() { + ConfigNode nodes = new ArrayNode(null); + SetDecoder decoder = new SetDecoder(); + + var gestaltConfig = new GestaltConfig(); + gestaltConfig.setTreatEmptyCollectionAsErrors(false); + decoder.applyConfig(gestaltConfig); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), nodes, new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(0, values.results().size()); + } + + @Test + void arrayDecodeEmptyLeafNodeOk() { + ConfigNode nodes = new LeafNode(""); + SetDecoder decoder = new SetDecoder(); + + var gestaltConfig = new GestaltConfig(); + gestaltConfig.setTreatEmptyCollectionAsErrors(false); + decoder.applyConfig(gestaltConfig); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), nodes, new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(1, values.results().size()); + Assertions.assertTrue(values.results().contains("")); + } + + @Test + void arrayDecodeNullLeafNodeOk() { + ConfigNode nodes = new LeafNode(null); + SetDecoder decoder = new SetDecoder(); + + var gestaltConfig = new GestaltConfig(); + gestaltConfig.setTreatEmptyCollectionAsErrors(false); + decoder.applyConfig(gestaltConfig); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), nodes, new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(0, values.results().size()); + } + + @Test + void arrayDecodeNullNodeOk() { + SetDecoder decoder = new SetDecoder(); + + var gestaltConfig = new GestaltConfig(); + gestaltConfig.setTreatEmptyCollectionAsErrors(false); + decoder.applyConfig(gestaltConfig); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), null, new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(0, values.results().size()); + } + @Test void arrayDecodeWrongTypeDoubles() { diff --git a/gestalt-core/src/test/java/org/github/gestalt/config/integration/GestaltIntegrationTests.java b/gestalt-core/src/test/java/org/github/gestalt/config/integration/GestaltIntegrationTests.java index f32f82f46..60cd2db76 100644 --- a/gestalt-core/src/test/java/org/github/gestalt/config/integration/GestaltIntegrationTests.java +++ b/gestalt-core/src/test/java/org/github/gestalt/config/integration/GestaltIntegrationTests.java @@ -477,6 +477,117 @@ public void integrationTestPostProcessorMulti() throws GestaltException { Assertions.assertEquals("booking", booking.getService().getPath()); } + @Test + public void testDontTreatEmptyCollectionAsErrors() throws GestaltException { + + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "3306"); + + GestaltBuilder builder = new GestaltBuilder(); + Gestalt gestalt = builder + .addSources(List.of(MapConfigSourceBuilder.builder().setCustomConfig(configs).build())) + .setTreatEmptyCollectionAsErrors(false) + .build(); + + gestalt.loadConfigs(); + + try { + List admins = gestalt.getConfig("admin", new TypeCapture<>() {}); + Assertions.assertEquals(0, admins.size()); + + Set adminsSet = gestalt.getConfig("admin", new TypeCapture<>() {}); + Assertions.assertEquals(0, adminsSet.size()); + } catch (GestaltException e) { + Assertions.fail("Should not reach here"); + } + } + + @Test + public void testTreatEmptyCollectionAsErrors() throws GestaltException { + + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "3306"); + + GestaltBuilder builder = new GestaltBuilder(); + Gestalt gestalt = builder + .addSources(List.of(MapConfigSourceBuilder.builder().setCustomConfig(configs).build())) + .setTreatEmptyCollectionAsErrors(true) + .build(); + + gestalt.loadConfigs(); + + try { + List admins = gestalt.getConfig("admin", new TypeCapture<>() {}); + Assertions.fail("Should not reach here"); + Assertions.assertEquals(0, admins.size()); // so it doesn't' complain about not being used. + } catch (GestaltException e) { + Assertions.assertEquals("Failed getting config path: admin, for class: java.util.List\n" + + " - level: MISSING_VALUE, message: Unable to find node matching path: admin, for class: ObjectToken, " + + "during navigating to next node", e.getMessage()); + } + + try { + Set admins = gestalt.getConfig("admin", new TypeCapture<>() {}); + Assertions.fail("Should not reach here"); + Assertions.assertEquals(0, admins.size()); // so it doesn't' complain about not being used. + } catch (GestaltException e) { + Assertions.assertEquals("Failed getting config path: admin, for class: java.util.Set\n" + + " - level: MISSING_VALUE, message: Unable to find node matching path: admin, for class: ObjectToken, " + + "during navigating to next node", e.getMessage()); + } + } + + @Test + public void testDontTreatEmptyArraysAsErrors() throws GestaltException { + + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "3306"); + + GestaltBuilder builder = new GestaltBuilder(); + Gestalt gestalt = builder + .addSources(List.of(MapConfigSourceBuilder.builder().setCustomConfig(configs).build())) + .setTreatEmptyCollectionAsErrors(false) + .build(); + + gestalt.loadConfigs(); + + try { + String[] admins = gestalt.getConfig("admin", TypeCapture.of(String[].class)); + Assertions.assertEquals(0, admins.length); + } catch (GestaltException e) { + Assertions.fail("Should not reach here"); + } + } + + @Test + public void testTreatEmptyArraysAsErrors() throws GestaltException { + + Map configs = new HashMap<>(); + configs.put("db.password", "test"); + configs.put("db.port", "3306"); + + GestaltBuilder builder = new GestaltBuilder(); + Gestalt gestalt = builder + .addSources(List.of(MapConfigSourceBuilder.builder().setCustomConfig(configs).build())) + .setTreatEmptyCollectionAsErrors(true) + .build(); + + gestalt.loadConfigs(); + + try { + String[] admins = gestalt.getConfig("admin", TypeCapture.of(String[].class)); + Assertions.fail("Should not reach here"); + Assertions.assertEquals(0, admins.length); // so it doesn't' complain about not being used. + } catch (GestaltException e) { + Assertions.assertEquals("Failed getting config path: admin, for class: java.lang.String[]\n" + + " - level: MISSING_VALUE, message: Unable to find node matching path: admin, " + + "for class: ObjectToken, during navigating to next node", e.getMessage()); + } + } + private void validateResults(Gestalt gestalt) throws GestaltException { HttpPool pool = gestalt.getConfig("http.pool", HttpPool.class); 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 68cbe0333..9d14ff406 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 @@ -276,6 +276,62 @@ public void integrationTestDefaultTags() throws GestaltException { validateResults(gestalt); } + + @Test + public void testDontTreatEmptyCollectionAsErrors() throws GestaltException { + + String hoconStr = "database: {\n" + + " global: {\n" + + " volumes: []\n" + + " }\n" + + "}\n"; + + GestaltBuilder builder = new GestaltBuilder(); + Gestalt gestalt = builder + .addSource(StringConfigSourceBuilder.builder().setConfig(hoconStr).setFormat("conf").build()) + .setTreatEmptyCollectionAsErrors(false) + .build(); + + gestalt.loadConfigs(); + + try { + List admins = gestalt.getConfig("database.global.volumes", new TypeCapture<>() {}); + Assertions.assertEquals(0, admins.size()); + } catch (GestaltException e) { + Assertions.fail("Should not reach here"); + } + } + + @Test + public void testTreatEmptyCollectionAsErrors() throws GestaltException { + + String hoconStr = "database: {\n" + + " global: {\n" + + " volumes: []\n" + + " }\n" + + "}\n"; + + GestaltBuilder builder = new GestaltBuilder(); + Gestalt gestalt = builder + .addSource(StringConfigSourceBuilder.builder().setConfig(hoconStr).setFormat("conf").build()) + .setTreatEmptyCollectionAsErrors(true) + .build(); + + gestalt.loadConfigs(); + + try { + List admins = gestalt.getConfig("database.global.volumes", new TypeCapture<>() {}); + Assertions.fail("Should not reach here"); + Assertions.assertEquals(0, admins.size()); // so it doesn't' complain about not being used. + } catch (GestaltException e) { + Assertions.assertEquals("Failed getting config path: database.global.volumes, " + + "for class: java.util.List\n" + + " - level: MISSING_VALUE, message: Array on path: database.global.volumes, has no value attempting to decode List", + e.getMessage()); + } + } + + @Test public void integrationTestProxyPassThrough() throws GestaltException { // Create a map of configurations we wish to inject.