From 6acf6b5791c624239cff2977ea14f59e33324ff6 Mon Sep 17 00:00:00 2001 From: Colin Redmond Date: Wed, 3 Jan 2024 16:03:36 -0800 Subject: [PATCH 1/9] feat: Support for SequencedSets, SequencedCollections and SequencedMap. Json, Toml, and Properties dont support sorted maps. Only Hocon supports sorted maps. --- README.md | 75 +-- .../decoder/SequencedCollectionDecoder.java | 75 +++ .../config/decoder/SequencedMapDecoder.java | 142 ++++++ .../config/decoder/SequencedSetDecoder.java | 72 +++ .../org.github.gestalt.config.decoder.Decoder | 3 + .../gestalt/config/hocon/HoconLoader.java | 2 +- .../config/decoder/DecoderOrderTest.java | 129 +++++ .../SequencedCollectionDecoderTest.java | 354 ++++++++++++++ .../decoder/SequencedMapDecoderTest.java | 441 ++++++++++++++++++ .../decoder/SequencedSetDecoderTest.java | 380 +++++++++++++++ .../src/test/resources/default.properties | 34 ++ .../src/test/resources/dev.properties | 18 + 12 files changed, 1688 insertions(+), 37 deletions(-) create mode 100644 gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedCollectionDecoder.java create mode 100644 gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedMapDecoder.java create mode 100644 gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedSetDecoder.java create mode 100644 gestalt-test/src/test/java/org/github/gestalt/config/decoder/DecoderOrderTest.java create mode 100644 gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedCollectionDecoderTest.java create mode 100644 gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedMapDecoderTest.java create mode 100644 gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedSetDecoderTest.java create mode 100644 gestalt-test/src/test/resources/default.properties create mode 100644 gestalt-test/src/test/resources/dev.properties diff --git a/README.md b/README.md index 4baab3b86..82bec8361 100644 --- a/README.md +++ b/README.md @@ -353,7 +353,7 @@ Example of how to create and load a configuration objects using Gestalt: // load a whole class, this works best with pojo's HttpPool pool = gestalt.getConfig("http.pool", HttpPool.class); // or get a spcific config value - short maxTotal gestalt.getConfig("http.pool.maxTotal", Short.class); + short maxTotal = gestalt.getConfig("http.pool.maxTotal", Short.class); // get with a default if you want a fallback from code long maxConnectionsPerRoute = gestalt.getConfig("http.pool.maxPerRoute", 24, Long.class); @@ -741,41 +741,44 @@ To register your own default ConfigLoaders add them to the builder, or add it to # Decoders -| Type | details | -|-------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| Array | Java primitive array type with any generic class, Can decode simple types from a single comma separated value, or from an array node. You can escape the comma with a \\, so the values are not split. | -| BigDecimal | | -| BigInteger | | -| Boolean | Boolean and boolean | -| Byte | Byte and byte | -| Char | Char and char | -| Date | takes a DateTimeFormatter as a parameter, by default it uses DateTimeFormatter.ISO_DATE_TIME | -| Double | Double and double | -| Duration | | -| Enum | | -| File | | -| Float | Float and float | -| Instant | | -| Integer | Integer and int | -| List | a Java list with any Generic class, Can decode simple types from a single comma separated value, or from an array node. You can escape the comma with a \\, so the values are not split. | -| LocalDate | Takes a DateTimeFormatter as a parameter, by default it uses DateTimeFormatter.ISO_LOCAL_DATE | -| LocalDateTime | Takes a DateTimeFormatter as a parameter, by default it uses DateTimeFormatter.ISO_DATE_TIME | -| Long | Long or long | -| Map | A map, Assumes that the key is a simple class that can be decoded from a single string. ie a Boolean, String, Int. The value can be any type we can decode. | -| Object | Decodes a java Bean style class, although it will work with any java class. Will fail if the constructor is private. Will construct the class even if there are missing values, the values will be null or the default. Then it will return errors which you can disable using treatMissingValuesAsErrors = true. Decodes member classes and lists as well. | -| Optional | Decodes an optional value, if no value is found it will return an Optional.empty() | -| OptionalDouble | Decodes an optional Double, if no value is found it will return an OptionalDouble.empty() | -| OptionaInt | Decodes an optional Integer, if no value is found it will return an OptionaInt.empty() | -| OptionalLong | Decodes an optional Long, if no value is found it will return an OptionalLong.empty() | -| Path | | -| Pattern | | -| Proxy (interface) | Will create a proxy for an interface that will return the config value based on the java bean method name. So a method "getCar()" would match a config named "car". If a config is missing it will call the default method if provided. Has 2 modes, Cached and pass-through, the default is Cached. Cached will receive a cache of all values on creation and return those from an internal cache. Pass-though will validate the object on creation, but when calling to get the values it will call gestalt for each value. This allows you to always get the most recent values. To set the mode on the builder use `Gestalt gestalt = builder.setProxyDecoderMode(ProxyDecoderMode.PASSTHROUGH)` | -| Record | Decodes a Java record. All members of the record must have a value or construction will fail.So unlike the Object decoder it will not have the option to default to null or provide defaults. Will construct the record even if there are extra values, it will ignore all extra values. | -| Set | a Java list with any Generic class, Can decode simple types from a single comma separated value, or from an array node | -| Short | Short or short | -| String | | -| StringConstructor | Will decode a class that has a constructor that accepts a single string. This will only match for leaf nodes. It will send the value of the leaf node to the String constructor. | -| UUID | | +| Type | details | +|---------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| Array | Java primitive array type with any generic class, Can decode simple types from a single comma separated value, or from an array node. You can escape the comma with a \\, so the values are not split. | +| BigDecimal | | +| BigInteger | | +| Boolean | Boolean and boolean | +| Byte | Byte and byte | +| Char | Char and char | +| Date | takes a DateTimeFormatter as a parameter, by default it uses DateTimeFormatter.ISO_DATE_TIME | +| Double | Double and double | +| Duration | | +| Enum | | +| File | | +| Float | Float and float | +| Instant | | +| Integer | Integer and int | +| List | a Java list with any Generic class, Can decode simple types from a single comma separated value, or from an array node. You can escape the comma with a \\, so the values are not split. | +| LocalDate | Takes a DateTimeFormatter as a parameter, by default it uses DateTimeFormatter.ISO_LOCAL_DATE | +| LocalDateTime | Takes a DateTimeFormatter as a parameter, by default it uses DateTimeFormatter.ISO_DATE_TIME | +| Long | Long or long | +| Map | A map, Assumes that the key is a simple class that can be decoded from a single string. ie a Boolean, String, Int. The value can be any type we can decode. | +| Object | Decodes a java Bean style class, although it will work with any java class. Will fail if the constructor is private. Will construct the class even if there are missing values, the values will be null or the default. Then it will return errors which you can disable using treatMissingValuesAsErrors = true. Decodes member classes and lists as well. | +| Optional | Decodes an optional value, if no value is found it will return an Optional.empty() | +| OptionalDouble | Decodes an optional Double, if no value is found it will return an OptionalDouble.empty() | +| OptionaInt | Decodes an optional Integer, if no value is found it will return an OptionaInt.empty() | +| OptionalLong | Decodes an optional Long, if no value is found it will return an OptionalLong.empty() | +| Path | | +| Pattern | | +| Proxy (interface) | Will create a proxy for an interface that will return the config value based on the java bean method name. So a method "getCar()" would match a config named "car". If a config is missing it will call the default method if provided. Has 2 modes, Cached and pass-through, the default is Cached. Cached will receive a cache of all values on creation and return those from an internal cache. Pass-though will validate the object on creation, but when calling to get the values it will call gestalt for each value. This allows you to always get the most recent values. To set the mode on the builder use `Gestalt gestalt = builder.setProxyDecoderMode(ProxyDecoderMode.PASSTHROUGH)` | +| Record | Decodes a Java record. All members of the record must have a value or construction will fail.So unlike the Object decoder it will not have the option to default to null or provide defaults. Will construct the record even if there are extra values, it will ignore all extra values. | +| SequencedCollection | A SequencedCollection with any Generic class, Can decode simple types from a single comma separated value, or from an array node. Provides a ArrayList. | +| SequencedMap | A map, Assumes that the key is a simple class that can be decoded from a single string. ie a Boolean, String, Int. The value can be any type we can decode. Provides a LinkedHashMap. Json, Toml, and Properties dont support sorted maps. Only Hocon supports sorted maps. | +| SequencedSet | A SequencedSet with any Generic class, Can decode simple types from a single comma separated value, or from an array node. Provides an ordered LinkedHashSet. | +| Set | A Set with any Generic class, Can decode simple types from a single comma separated value, or from an array node. Provides an unordered HashSet. | +| Short | Short or short | +| String | | +| StringConstructor | Will decode a class that has a constructor that accepts a single string. This will only match for leaf nodes. It will send the value of the leaf node to the String constructor. | +| UUID | | For Kotlin, the kotlin specific decoders are only selected when calling from the Kotlin Gestalt extension functions, or when using KTypeCapture. Otherwise, it will match the Java decoder. Kotlin decoders: Boolean, Byte, Char, Data class, Double, Duration, Float, Integer, Long, Short, String diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedCollectionDecoder.java b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedCollectionDecoder.java new file mode 100644 index 000000000..6289bae9f --- /dev/null +++ b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedCollectionDecoder.java @@ -0,0 +1,75 @@ +package org.github.gestalt.config.decoder; + +import org.github.gestalt.config.entity.ValidationError; +import org.github.gestalt.config.node.ConfigNode; +import org.github.gestalt.config.reflect.TypeCapture; +import org.github.gestalt.config.tag.Tags; +import org.github.gestalt.config.utils.PathUtil; +import org.github.gestalt.config.utils.ValidateOf; + +import java.util.ArrayList; +import java.util.List; + +/** + * Decode a Sequenced list type. + * + * @author Colin Redmond (c) 2023. + */ +public final class SequencedCollectionDecoder extends CollectionDecoder> { + + private static final System.Logger logger = System.getLogger(SequencedCollectionDecoder.class.getName()); + + Class sequencedCollection; + + public SequencedCollectionDecoder() { + try { + sequencedCollection = Class.forName("java.util.SequencedCollection"); + } catch (ClassNotFoundException e) { + sequencedCollection = null; + logger.log(System.Logger.Level.TRACE, "Unable to find class java.util.SequencedCollection, SequencedCollectionDecoder disabled"); + } + } + + @Override + public String name() { + return "SequencedCollection"; + } + + @Override + public Priority priority() { + return Priority.HIGH; + } + + @Override + public boolean canDecode(String path, Tags tags, ConfigNode node, TypeCapture type) { + return sequencedCollection != null && sequencedCollection.equals(type.getRawType()) && type.hasParameter(); + } + + @Override + protected ValidateOf> arrayDecode(String path, Tags tags, ConfigNode node, TypeCapture klass, + DecoderContext decoderContext) { + List errors = new ArrayList<>(); + List results = new ArrayList<>(node.size()); + + for (int i = 0; i < node.size(); i++) { + if (node.getIndex(i).isPresent()) { + ConfigNode currentNode = node.getIndex(i).get(); + String nextPath = PathUtil.pathForIndex(path, i); + ValidateOf validateOf = decoderContext.getDecoderService() + .decodeNode(nextPath, tags, currentNode, klass.getFirstParameterType(), decoderContext); + + errors.addAll(validateOf.getErrors()); + if (validateOf.hasResults()) { + results.add(validateOf.results()); + } + + } else { + errors.add(new ValidationError.ArrayMissingIndex(i)); + results.add(null); + } + } + + + return ValidateOf.validateOf(results, errors); + } +} diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedMapDecoder.java b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedMapDecoder.java new file mode 100644 index 000000000..1299037cb --- /dev/null +++ b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedMapDecoder.java @@ -0,0 +1,142 @@ +package org.github.gestalt.config.decoder; + +import org.github.gestalt.config.entity.ValidationError; +import org.github.gestalt.config.node.ArrayNode; +import org.github.gestalt.config.node.ConfigNode; +import org.github.gestalt.config.node.LeafNode; +import org.github.gestalt.config.node.MapNode; +import org.github.gestalt.config.reflect.TypeCapture; +import org.github.gestalt.config.tag.Tags; +import org.github.gestalt.config.utils.ClassUtils; +import org.github.gestalt.config.utils.Pair; +import org.github.gestalt.config.utils.PathUtil; +import org.github.gestalt.config.utils.ValidateOf; + +import java.util.*; +import java.util.stream.Stream; + +/** + * Decode a Map. Assumes that the key is a simple class that can be decoded from a single string. ie a Boolean, String, Int. + * The value can be any type we can decode. + * + * @author Colin Redmond (c) 2023. + */ +public final class SequencedMapDecoder implements Decoder> { + + private static final System.Logger logger = System.getLogger(SequencedMapDecoder.class.getName()); + + Class sequencedMap; + + public SequencedMapDecoder() { + try { + sequencedMap = Class.forName("java.util.SequencedMap"); + } catch (ClassNotFoundException e) { + sequencedMap = null; + logger.log(System.Logger.Level.TRACE, "Unable to find class java.util.SequencedMap, SequencedMapDecoder disabled"); + } + } + + @Override + public Priority priority() { + return Priority.HIGH; + } + + @Override + public String name() { + return "SequencedMap"; + } + + @Override + public boolean canDecode(String path, Tags tags, ConfigNode node, TypeCapture type) { + return sequencedMap != null && sequencedMap.isAssignableFrom(type.getRawType()) && type.hasParameter(); + } + + @Override + public ValidateOf> decode(String path, Tags tags, ConfigNode node, TypeCapture type, DecoderContext decoderContext) { + ValidateOf> results; + if (node instanceof MapNode) { + MapNode mapNode = (MapNode) node; + List> genericInterfaces = type.getParameterTypes(); + + if (genericInterfaces == null || genericInterfaces.size() != 2) { + results = ValidateOf.inValid(new ValidationError.DecodingExpectedMapNodeType(path, genericInterfaces, node)); + } else { + TypeCapture keyType = genericInterfaces.get(0); + TypeCapture valueType = genericInterfaces.get(1); + + List errors = new ArrayList<>(); + + var stream = mapNode.getMapNode().entrySet().stream(); + + // if the value of the map is a primitive or a wrapper, flat map any entries that are map nodes. + // if the value is a class, then we want to decode the map nodes into an object + if (ClassUtils.isPrimitiveOrWrapper(valueType.getRawType())) { + stream = stream.flatMap(it -> convertMapToStream(it.getKey(), it)); + } + + Map map = stream.map(it -> { + String key = it.getKey(); + if (key == null) { + errors.add(new ValidationError.DecodersMapKeyNull(path)); + return null; + } + + String nextPath = PathUtil.pathForKey(path, key); + ValidateOf keyValidate = decoderContext.getDecoderService() + .decodeNode(nextPath, tags, new LeafNode(key), (TypeCapture) keyType, decoderContext); + ValidateOf valueValidate = decoderContext.getDecoderService() + .decodeNode(nextPath, tags, it.getValue(), (TypeCapture) valueType, decoderContext); + + errors.addAll(keyValidate.getErrors()); + errors.addAll(valueValidate.getErrors()); + + if (!keyValidate.hasResults()) { + errors.add(new ValidationError.DecodersMapKeyNull(nextPath)); + } + if (!valueValidate.hasResults()) { + errors.add(new ValidationError.DecodersMapValueNull(nextPath)); + } + + if (keyValidate.hasResults()) { + return new Pair<>(keyValidate.results(), valueValidate.results()); + } + return null; + }) + .filter(Objects::nonNull) + .collect(LinkedHashMap::new, (m, v) -> m.put(v.getFirst(), v.getSecond()), LinkedHashMap::putAll); + + + return ValidateOf.validateOf(map, errors); + } + } else { + return ValidateOf.inValid(new ValidationError.DecodingExpectedMapNodeType(path, node)); + } + return results; + } + + private Stream> convertMapToStream(String path, Map.Entry entry) { + // if the key or entry is null, return the current entry and let later code deal with the null value. + if (path == null || entry.getValue() == null) { + return Stream.of(entry); + } else if (entry.getValue() instanceof MapNode) { + MapNode node = (MapNode) entry.getValue(); + + return node.getMapNode().entrySet().stream().flatMap(it -> convertMapToStream(path + "." + it.getKey(), it)); + } else if (entry.getValue() instanceof ArrayNode) { + ArrayNode node = (ArrayNode) entry.getValue(); + + Stream> stream = Stream.of(); + List nodes = node.getArray(); + + for (int i = 0; i < nodes.size(); i++) { + stream = Stream.concat(stream, convertMapToStream(path + "[" + i + "]", Map.entry("[" + i + "]", nodes.get(i)))); + } + + return stream; + } else if (entry.getValue() instanceof LeafNode) { + return Stream.of(Map.entry(path, entry.getValue())); + } else { + return Stream.of(); + } + } +} diff --git a/gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedSetDecoder.java b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedSetDecoder.java new file mode 100644 index 000000000..dbfe1780d --- /dev/null +++ b/gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedSetDecoder.java @@ -0,0 +1,72 @@ +package org.github.gestalt.config.decoder; + +import org.github.gestalt.config.entity.ValidationError; +import org.github.gestalt.config.node.ConfigNode; +import org.github.gestalt.config.reflect.TypeCapture; +import org.github.gestalt.config.tag.Tags; +import org.github.gestalt.config.utils.PathUtil; +import org.github.gestalt.config.utils.ValidateOf; + +import java.util.*; + +/** + * Decode a Sequenced Set type. + * + * @author Colin Redmond (c) 2023. + */ +public final class SequencedSetDecoder extends CollectionDecoder> { + + private static final System.Logger logger = System.getLogger(SequencedSetDecoder.class.getName()); + + Class sequencedSet; + + public SequencedSetDecoder() { + try { + sequencedSet = Class.forName("java.util.SequencedSet"); + } catch (ClassNotFoundException e) { + sequencedSet = null; + logger.log(System.Logger.Level.TRACE, "Unable to find class java.util.SequencedSet, SequencedSetDecoder disabled"); + } + } + + @Override + public String name() { + return "SequencedSet"; + } + + @Override + public Priority priority() { + return Priority.HIGH; + } + + @Override + public boolean canDecode(String path, Tags tags, ConfigNode node, TypeCapture type) { + return sequencedSet != null && sequencedSet.isAssignableFrom(type.getRawType()) && type.hasParameter(); + } + + @Override + protected ValidateOf> arrayDecode(String path, Tags tags, ConfigNode node, TypeCapture klass, DecoderContext decoderContext) { + List errors = new ArrayList<>(); + Set results = new LinkedHashSet<>(node.size()); + + for (int i = 0; i < node.size(); i++) { + if (node.getIndex(i).isPresent()) { + ConfigNode currentNode = node.getIndex(i).get(); + String nextPath = PathUtil.pathForIndex(path, i); + ValidateOf validateOf = decoderContext.getDecoderService() + .decodeNode(nextPath, tags, currentNode, klass.getFirstParameterType(), decoderContext); + + errors.addAll(validateOf.getErrors()); + if (validateOf.hasResults()) { + results.add(validateOf.results()); + } + + } else { + errors.add(new ValidationError.ArrayMissingIndex(i, path)); + } + } + + + return ValidateOf.validateOf(results, errors); + } +} diff --git a/gestalt-core/src/main/resources/META-INF/services/org.github.gestalt.config.decoder.Decoder b/gestalt-core/src/main/resources/META-INF/services/org.github.gestalt.config.decoder.Decoder index 3a335d950..af1c69253 100644 --- a/gestalt-core/src/main/resources/META-INF/services/org.github.gestalt.config.decoder.Decoder +++ b/gestalt-core/src/main/resources/META-INF/services/org.github.gestalt.config.decoder.Decoder @@ -27,6 +27,9 @@ org.github.gestalt.config.decoder.PatternDecoder org.github.gestalt.config.decoder.ProxyDecoder org.github.gestalt.config.decoder.RecordDecoder org.github.gestalt.config.decoder.SetDecoder +org.github.gestalt.config.decoder.SequencedCollectionDecoder +org.github.gestalt.config.decoder.SequencedMapDecoder +org.github.gestalt.config.decoder.SequencedSetDecoder org.github.gestalt.config.decoder.ShortDecoder org.github.gestalt.config.decoder.StringDecoder org.github.gestalt.config.decoder.StringConstructorDecoder diff --git a/gestalt-hocon/src/main/java/org/github/gestalt/config/hocon/HoconLoader.java b/gestalt-hocon/src/main/java/org/github/gestalt/config/hocon/HoconLoader.java index f27aa677f..fb530c438 100644 --- a/gestalt-hocon/src/main/java/org/github/gestalt/config/hocon/HoconLoader.java +++ b/gestalt-hocon/src/main/java/org/github/gestalt/config/hocon/HoconLoader.java @@ -137,7 +137,7 @@ private ValidateOf buildArrayConfigTree(String path, ConfigList conf private ValidateOf buildObjectConfigTree(String path, ConfigObject configObject) { List errors = new ArrayList<>(); - Map mapNode = new HashMap<>(); + Map mapNode = new LinkedHashMap<>(); configObject.forEach((key, value) -> { String newPath = normalizeSentence(key); diff --git a/gestalt-test/src/test/java/org/github/gestalt/config/decoder/DecoderOrderTest.java b/gestalt-test/src/test/java/org/github/gestalt/config/decoder/DecoderOrderTest.java new file mode 100644 index 000000000..c3fadc1d6 --- /dev/null +++ b/gestalt-test/src/test/java/org/github/gestalt/config/decoder/DecoderOrderTest.java @@ -0,0 +1,129 @@ +package org.github.gestalt.config.decoder; + +import org.github.gestalt.config.Gestalt; +import org.github.gestalt.config.builder.GestaltBuilder; +import org.github.gestalt.config.exceptions.GestaltException; +import org.github.gestalt.config.reflect.TypeCapture; +import org.github.gestalt.config.source.ClassPathConfigSourceBuilder; +import org.github.gestalt.config.source.FileConfigSourceBuilder; +import org.github.gestalt.config.source.MapConfigSourceBuilder; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.net.URL; +import java.util.*; + +/** + * @author Colin Redmond (c) 2023. + */ +public class DecoderOrderTest { + + @Test + public void sequencedSetAndList() throws GestaltException { + // Create a map of configurations we wish to inject. + Map configs = new HashMap<>(); + configs.put("db.hosts[0].password", "1234"); + configs.put("db.hosts[1].password", "5678"); + configs.put("db.hosts[2].password", "9012"); + configs.put("db.idleTimeout", "123"); + + // Load the default property files from resources. + URL devFileURL = DecoderOrderTest.class.getClassLoader().getResource("dev.properties"); + File devFile = new File(devFileURL.getFile()); + + // 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(FileConfigSourceBuilder.builder().setFile(devFile).build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .setTreatNullValuesInClassAsErrors(false) + .build(); + + // Load the configurations, this will throw exceptions if there are any errors. + gestalt.loadConfigs(); + + List hosts = gestalt.getConfig("db.hosts", new TypeCapture<>() { }); + + Assertions.assertEquals(1234, hosts.get(0).password); + Assertions.assertEquals(5678, hosts.get(1).password); + Assertions.assertEquals(9012, hosts.get(2).password); + + ArrayList hostsArray = gestalt.getConfig("db.hosts", new TypeCapture<>() { }); + + Assertions.assertEquals(1234, hostsArray.get(0).password); + Assertions.assertEquals(5678, hostsArray.get(1).password); + Assertions.assertEquals(9012, hostsArray.get(2).password); + + SequencedSet hostsSet = gestalt.getConfig("db.hosts", new TypeCapture<>() { }); + + Assertions.assertEquals(1234, hostsSet.removeFirst().password); + Assertions.assertEquals(5678, hostsSet.removeFirst().password); + Assertions.assertEquals(9012, hostsSet.removeFirst().password); + + LinkedHashSet hostsLinkedSet = gestalt.getConfig("db.hosts", new TypeCapture<>() { }); + + Assertions.assertEquals(1234, hostsLinkedSet.removeFirst().password); + Assertions.assertEquals(5678, hostsLinkedSet.removeFirst().password); + Assertions.assertEquals(9012, hostsLinkedSet.removeFirst().password); + + SequencedCollection hostsCollection = gestalt.getConfig("db.hosts", new TypeCapture<>() { }); + + Assertions.assertEquals(1234, hostsCollection.removeFirst().password); + Assertions.assertEquals(5678, hostsCollection.removeFirst().password); + Assertions.assertEquals(9012, hostsCollection.removeFirst().password); + } + + @Test + public void sequencedMap() throws GestaltException { + // Create a map of configurations we wish to inject. + Map configs = new HashMap<>(); + configs.put("db.hosts[0].password", "1234"); + configs.put("db.hosts[1].password", "5678"); + configs.put("db.hosts[2].password", "9012"); + configs.put("db.idleTimeout", "123"); + + // Load the default property files from resources. + URL devFileURL = DecoderOrderTest.class.getClassLoader().getResource("dev.properties"); + File devFile = new File(devFileURL.getFile()); + + // 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(FileConfigSourceBuilder.builder().setFile(devFile).build()) + .addSource(MapConfigSourceBuilder.builder().setCustomConfig(configs).build()) + .setTreatNullValuesInClassAsErrors(false) + .build(); + + // Load the configurations, this will throw exceptions if there are any errors. + gestalt.loadConfigs(); + + Map httpPoolMap = gestalt.getConfig("http.pool", new TypeCapture<>() { }); + + Assertions.assertEquals(50, httpPoolMap.get("maxperroute")); + Assertions.assertEquals(6000, httpPoolMap.get("validateafterinactivity")); + Assertions.assertEquals(60000, httpPoolMap.get("keepalivetimeoutms")); + Assertions.assertEquals(25, httpPoolMap.get("idletimeoutsec")); + + SequencedMap httpPoolMapSeq = gestalt.getConfig("http.pool", new TypeCapture<>() { }); + + Assertions.assertEquals(50, httpPoolMapSeq.get("maxperroute")); + Assertions.assertEquals(6000, httpPoolMapSeq.get("validateafterinactivity")); + Assertions.assertEquals(60000, httpPoolMapSeq.get("keepalivetimeoutms")); + Assertions.assertEquals(25, httpPoolMapSeq.get("idletimeoutsec")); + + LinkedHashMap httpPoolMapLinkedSeq = gestalt.getConfig("http.pool", new TypeCapture<>() { }); + + Assertions.assertEquals(50, httpPoolMapLinkedSeq.get("maxperroute")); + Assertions.assertEquals(6000, httpPoolMapLinkedSeq.get("validateafterinactivity")); + Assertions.assertEquals(60000, httpPoolMapLinkedSeq.get("keepalivetimeoutms")); + Assertions.assertEquals(25, httpPoolMapLinkedSeq.get("idletimeoutsec")); + } + + public record Hosts(Integer password, String user, String url) { } + +} diff --git a/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedCollectionDecoderTest.java b/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedCollectionDecoderTest.java new file mode 100644 index 000000000..18f3abee8 --- /dev/null +++ b/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedCollectionDecoderTest.java @@ -0,0 +1,354 @@ +package org.github.gestalt.config.decoder; + +import org.github.gestalt.config.exceptions.GestaltConfigurationException; +import org.github.gestalt.config.lexer.SentenceLexer; +import org.github.gestalt.config.node.*; +import org.github.gestalt.config.path.mapper.StandardPathMapper; +import org.github.gestalt.config.reflect.TypeCapture; +import org.github.gestalt.config.tag.Tags; +import org.github.gestalt.config.utils.ValidateOf; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.*; + +class SequencedCollectionDecoderTest { + final DoubleDecoder doubleDecoder = new DoubleDecoder(); + final StringDecoder stringDecoder = new StringDecoder(); + final SequencedCollectionDecoder SequencedCollectionDecoder = new SequencedCollectionDecoder(); + + final ListDecoder listDecoder = new ListDecoder(); + + ConfigNodeService configNodeService; + DecoderService decoderService; + SentenceLexer lexer; + + @BeforeEach + void setup() throws GestaltConfigurationException { + configNodeService = Mockito.mock(ConfigNodeService.class); + lexer = Mockito.mock(SentenceLexer.class); + decoderService = new DecoderRegistry(List.of(doubleDecoder, stringDecoder, SequencedCollectionDecoder, listDecoder), configNodeService, lexer, + List.of(new StandardPathMapper())); + } + + @Test + void name() { + + SequencedCollectionDecoder decoder = new SequencedCollectionDecoder(); + Assertions.assertEquals("SequencedList", decoder.name()); + } + + @Test + void priority() { + SequencedCollectionDecoder decoder = new SequencedCollectionDecoder(); + Assertions.assertEquals(Priority.HIGH, decoder.priority()); + } + + @Test + void canDecode() { + SequencedCollectionDecoder decoder = new SequencedCollectionDecoder(); + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), TypeCapture.of(String.class))); + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture() { + })); + + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), TypeCapture.of(Long.class))); + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture() { + })); + + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), TypeCapture.of(List.class))); + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture>() { + })); + + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture>() { + })); + + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), TypeCapture.of(Set.class))); + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture>() { + })); + + Assertions.assertTrue(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture>() { + })); + + } + + @Test + void arrayDecodeStrings() { + + ConfigNode[] arrayNode = new ConfigNode[3]; + arrayNode[0] = new LeafNode("John"); + arrayNode[1] = new LeafNode("Steve"); + arrayNode[2] = new LeafNode("Matt"); + + ConfigNode nodes = new ArrayNode(Arrays.asList(arrayNode)); + SequencedCollectionDecoder decoder = new SequencedCollectionDecoder(); + + 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(3, values.results().size()); + Assertions.assertEquals("John", values.results().get(0)); + Assertions.assertEquals("Steve", values.results().get(1)); + Assertions.assertEquals("Matt", values.results().get(2)); + + SequencedCollection sq = values.results(); + Assertions.assertEquals(3, sq.size()); + Assertions.assertEquals("John", sq.removeFirst()); + Assertions.assertEquals("Steve", sq.removeFirst()); + Assertions.assertEquals("Matt", sq.removeFirst()); + } + + @Test + void arrayDecodeDoubles() { + + ConfigNode[] arrayNode = new ConfigNode[3]; + arrayNode[0] = new LeafNode("0.1111"); + arrayNode[1] = new LeafNode("0.222"); + arrayNode[2] = new LeafNode("0.33"); + + ConfigNode nodes = new ArrayNode(List.of(arrayNode)); + SequencedCollectionDecoder decoder = new SequencedCollectionDecoder(); + + 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(3, values.results().size()); + + Assertions.assertEquals(0.1111, values.results().get(0)); + Assertions.assertEquals(0.222, values.results().get(1)); + Assertions.assertEquals(0.33, values.results().get(2)); + + SequencedCollection sq = values.results(); + Assertions.assertEquals(3, sq.size()); + Assertions.assertEquals(0.1111, sq.removeFirst()); + Assertions.assertEquals(0.222, sq.removeFirst()); + Assertions.assertEquals(0.33, sq.removeFirst()); + } + + @Test + void arrayDecodeDoublesMissingIndex() { + + ConfigNode[] arrayNode = new ConfigNode[4]; + arrayNode[0] = new LeafNode("0.1111"); + arrayNode[1] = new LeafNode("0.222"); + arrayNode[3] = new LeafNode("0.33"); + + ConfigNode nodes = new ArrayNode(Arrays.asList(arrayNode)); + SequencedCollectionDecoder decoder = new SequencedCollectionDecoder(); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), nodes, new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertTrue(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(4, values.results().size()); + Assertions.assertEquals(1, values.getErrors().size()); + Assertions.assertEquals("Missing array index: 2", values.getErrors().get(0).description()); + + Assertions.assertEquals(0.1111, values.results().get(0)); + Assertions.assertEquals(0.222, values.results().get(1)); + Assertions.assertNull(values.results().get(2)); + Assertions.assertEquals(0.33, values.results().get(3)); + + SequencedCollection sq = values.results(); + Assertions.assertEquals(0.1111, sq.removeFirst()); + Assertions.assertEquals(0.222, sq.removeFirst()); + Assertions.assertNull(sq.removeFirst()); + Assertions.assertEquals(0.33, sq.removeFirst()); + } + + @Test + void arrayDecodeLeaf() { + SequencedCollectionDecoder decoder = new SequencedCollectionDecoder(); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), new LeafNode("0.1111, 0.22"), + new TypeCapture>() { }, new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + + Assertions.assertEquals(2, values.results().size()); + Assertions.assertEquals(0.1111, values.results().get(0)); + Assertions.assertEquals(0.22, values.results().get(1)); + + SequencedCollection sq = values.results(); + Assertions.assertEquals(2, sq.size()); + Assertions.assertEquals(0.1111, sq.removeFirst()); + Assertions.assertEquals(0.22, sq.removeFirst()); + } + + @Test + void arrayDecodeLeafWithEscapeComma() { + SequencedCollectionDecoder decoder = new SequencedCollectionDecoder(); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), new LeafNode("a,b,c\\,d"), + new TypeCapture>() { }, new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + + Assertions.assertEquals(3, values.results().size()); + Assertions.assertEquals("a", values.results().get(0)); + Assertions.assertEquals("b", values.results().get(1)); + Assertions.assertEquals("c,d", values.results().get(2)); + + SequencedCollection sq = values.results(); + Assertions.assertEquals(3, sq.size()); + Assertions.assertEquals("a", sq.removeFirst()); + Assertions.assertEquals("b", sq.removeFirst()); + Assertions.assertEquals("c,d", sq.removeFirst()); + } + + @Test + void arrayDecodeNullLeaf() { + SequencedCollectionDecoder decoder = new SequencedCollectionDecoder(); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), new LeafNode(null), + new TypeCapture>() { }, new DecoderContext(decoderService, null)); + + Assertions.assertTrue(values.hasErrors()); + Assertions.assertFalse(values.hasResults()); + + Assertions.assertEquals(1, values.getErrors().size()); + Assertions.assertEquals("Leaf on path: db.hosts, has no value attempting to decode SequencedList", + values.getErrors().get(0).description()); + } + + @Test + void arrayDecodeNullNode() { + SequencedCollectionDecoder decoder = new SequencedCollectionDecoder(); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), null, + new TypeCapture>() { }, new DecoderContext(decoderService, null)); + + Assertions.assertTrue(values.hasErrors()); + Assertions.assertFalse(values.hasResults()); + + Assertions.assertEquals(1, values.getErrors().size()); + Assertions.assertEquals("Expected a Array on path: db.hosts, received node type: null, attempting to decode SequencedList", + values.getErrors().get(0).description()); + } + + @Test + void arrayDecodeEmptyArrayNodeOk() { + ConfigNode nodes = new ArrayNode(List.of()); + SequencedCollectionDecoder decoder = new SequencedCollectionDecoder(); + + 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); + SequencedCollectionDecoder decoder = new SequencedCollectionDecoder(); + + 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(""); + SequencedCollectionDecoder decoder = new SequencedCollectionDecoder(); + + 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)); + + SequencedCollection sq = values.results(); + Assertions.assertEquals(1, sq.size()); + Assertions.assertEquals("", sq.removeFirst()); + } + + + @Test + void arrayDecodeWrongTypeDoubles() { + + ConfigNode[] arrayNode = new ConfigNode[3]; + arrayNode[0] = new LeafNode("John"); + arrayNode[1] = new LeafNode("Matt"); + arrayNode[2] = new LeafNode("Tom"); + + ConfigNode nodes = new ArrayNode(Arrays.asList(arrayNode)); + SequencedCollectionDecoder decoder = new SequencedCollectionDecoder(); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), nodes, + new TypeCapture>() { }, new DecoderContext(decoderService, null)); + + Assertions.assertTrue(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(0, values.results().size()); + + Assertions.assertEquals(3, values.getErrors().size()); + Assertions.assertEquals("Unable to parse a number on Path: db.hosts[0], from node: LeafNode{value='John'} " + + "attempting to decode Double", + values.getErrors().get(0).description()); + Assertions.assertEquals("Unable to parse a number on Path: db.hosts[1], from node: LeafNode{value='Matt'} " + + "attempting to decode Double", + values.getErrors().get(1).description()); + Assertions.assertEquals("Unable to parse a number on Path: db.hosts[2], from node: LeafNode{value='Tom'} " + + "attempting to decode Double", + values.getErrors().get(2).description()); + } + + @Test + void arrayDecodeMixedWrongTypeDoubles() { + + ConfigNode[] arrayNode = new ConfigNode[3]; + arrayNode[0] = new LeafNode("John"); + arrayNode[1] = new LeafNode("0.22"); + arrayNode[2] = new LeafNode("Tom"); + + ConfigNode nodes = new ArrayNode(Arrays.asList(arrayNode)); + SequencedCollectionDecoder decoder = new SequencedCollectionDecoder(); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), nodes, + new TypeCapture>() { }, new DecoderContext(decoderService, null)); + + Assertions.assertTrue(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + + Assertions.assertEquals(2, values.getErrors().size()); + Assertions.assertEquals("Unable to parse a number on Path: db.hosts[0], from node: LeafNode{value='John'} " + + "attempting to decode Double", + values.getErrors().get(0).description()); + Assertions.assertEquals("Unable to parse a number on Path: db.hosts[2], from node: " + + "LeafNode{value='Tom'} attempting to decode Double", + values.getErrors().get(1).description()); + + Assertions.assertEquals(0.22, values.results().get(0)); + } + + @Test + void arrayDecodeMapNode() { + SequencedCollectionDecoder decoder = new SequencedCollectionDecoder(); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), new MapNode(new HashMap<>()), + new TypeCapture>() { }, new DecoderContext(decoderService, null)); + + Assertions.assertTrue(values.hasErrors()); + Assertions.assertFalse(values.hasResults()); + + Assertions.assertEquals(1, values.getErrors().size()); + Assertions.assertEquals("Expected a Array on path: db.hosts, received node type: MAP, attempting to decode SequencedList", + values.getErrors().get(0).description()); + } +} diff --git a/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedMapDecoderTest.java b/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedMapDecoderTest.java new file mode 100644 index 000000000..adb2b451b --- /dev/null +++ b/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedMapDecoderTest.java @@ -0,0 +1,441 @@ +package org.github.gestalt.config.decoder; + +import org.github.gestalt.config.exceptions.GestaltConfigurationException; +import org.github.gestalt.config.lexer.PathLexer; +import org.github.gestalt.config.lexer.SentenceLexer; +import org.github.gestalt.config.node.*; +import org.github.gestalt.config.path.mapper.StandardPathMapper; +import org.github.gestalt.config.reflect.TypeCapture; +import org.github.gestalt.config.tag.Tags; +import org.github.gestalt.config.test.classes.DBInfo; +import org.github.gestalt.config.utils.ValidateOf; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.*; + +class SequencedMapDecoderTest { + + final SentenceLexer lexer = new PathLexer(); + ConfigNodeService configNodeService; + DecoderRegistry decoderService; + + @BeforeEach + void setup() throws GestaltConfigurationException { + configNodeService = new ConfigNodeManager(); + decoderService = new DecoderRegistry(List.of(new LongDecoder(), new IntegerDecoder(), new StringDecoder(), + new ObjectDecoder(), new FloatDecoder(), new ListDecoder(), new SequencedMapDecoder()), configNodeService, lexer, + List.of(new StandardPathMapper())); + } + + @Test + void name() { + SequencedMapDecoder decoder = new SequencedMapDecoder(); + Assertions.assertEquals("SequencedMap", decoder.name()); + } + + @Test + void priority() { + SequencedMapDecoder decoder = new SequencedMapDecoder(); + Assertions.assertEquals(Priority.HIGH, decoder.priority()); + } + + @Test + void canDecode() { + SequencedMapDecoder decoder = new SequencedMapDecoder(); + + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture>() { + })); + + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture>() { + })); + + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), TypeCapture.of(DBInfo.class))); + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), TypeCapture.of(Long.class))); + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture() { + })); + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), TypeCapture.of(long.class))); + + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), TypeCapture.of(String.class))); + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), TypeCapture.of(Date.class))); + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture>() { + })); + + Assertions.assertTrue(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture>() { + })); + + Assertions.assertTrue(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture>() { + })); + + } + + @Test + void decode() { + + Map configs = new LinkedHashMap<>(); + configs.put("port", new LeafNode("100")); + configs.put("uri", new LeafNode("300")); + configs.put("password", new LeafNode("6000")); + + SequencedMapDecoder decoder = new SequencedMapDecoder(); + + ValidateOf> validate = decoder.decode("db.host", Tags.of(), new MapNode(configs), + new TypeCapture>() { }, new DecoderContext(decoderService, null)); + Assertions.assertTrue(validate.hasResults()); + Assertions.assertFalse(validate.hasErrors()); + + Map results = (Map) validate.results(); + Assertions.assertEquals(100, results.get("port")); + Assertions.assertEquals(300, results.get("uri")); + Assertions.assertEquals(6000, results.get("password")); + + SequencedSet> results2 = ((SequencedMap) validate.results()).sequencedEntrySet(); + Assertions.assertEquals(3, results2.size()); + Assertions.assertEquals(Map.entry("port", 100), results2.removeFirst()); + Assertions.assertEquals(Map.entry("uri", 300), results2.removeFirst()); + Assertions.assertEquals(Map.entry("password", 6000), results2.removeFirst()); + } + + + @Test + void decodeInt() { + + Map configs = new LinkedHashMap<>(); + configs.put("1", new LeafNode("100")); + configs.put("2", new LeafNode("300")); + configs.put("3", new LeafNode("6000")); + + SequencedMapDecoder decoder = new SequencedMapDecoder(); + + ValidateOf> validate = decoder.decode("db.host", Tags.of(), new MapNode(configs), + new TypeCapture>() { }, new DecoderContext(decoderService, null)); + Assertions.assertTrue(validate.hasResults()); + Assertions.assertFalse(validate.hasErrors()); + + Map results = (Map) validate.results(); + Assertions.assertEquals(100, results.get(1)); + Assertions.assertEquals(300, results.get(2)); + Assertions.assertEquals(6000, results.get(3)); + + SequencedSet> results2 = ((SequencedMap) validate.results()).sequencedEntrySet(); + Assertions.assertEquals(3, results2.size()); + Assertions.assertEquals(Map.entry(1, 100), results2.removeFirst()); + Assertions.assertEquals(Map.entry(2, 300), results2.removeFirst()); + Assertions.assertEquals(Map.entry(3, 6000), results2.removeFirst()); + } + + + @Test + void decodeClass() { + Map user1 = new LinkedHashMap<>(); + user1.put("name", new LeafNode("steve")); + user1.put("age", new LeafNode("52")); + Map user2 = new LinkedHashMap<>(); + user2.put("name", new LeafNode("john")); + user2.put("age", new LeafNode("23")); + + Map configs = new LinkedHashMap<>(); + configs.put("user1", new MapNode(user1)); + configs.put("user2", new MapNode(user2)); + + SequencedMapDecoder decoder = new SequencedMapDecoder(); + + ValidateOf> validate = decoder.decode("db.host", Tags.of(), new MapNode(configs), + new TypeCapture>() { }, new DecoderContext(decoderService, null)); + Assertions.assertTrue(validate.hasResults()); + Assertions.assertFalse(validate.hasErrors()); + + Map results = (Map) validate.results(); + Assertions.assertEquals("steve", results.get("user1").name); + Assertions.assertEquals(52, results.get("user1").age); + Assertions.assertEquals("john", results.get("user2").name); + Assertions.assertEquals(23, results.get("user2").age); + + SequencedSet> results2 = ((SequencedMap) validate.results()).sequencedEntrySet(); + Assertions.assertEquals(2, results2.size()); + Assertions.assertEquals(Map.entry("user1", new User("steve", 52)), results2.removeFirst()); + Assertions.assertEquals(Map.entry("user2", new User("john", 23)), results2.removeFirst()); + } + + @Test + void decodeNestedMap() { + Map retrySetting = new LinkedHashMap<>(); + retrySetting.put("times", new LeafNode("2")); + retrySetting.put("delay", new LeafNode("7")); + + Map settings = new LinkedHashMap<>(); + settings.put("timeout", new LeafNode("123")); + settings.put("retry", new MapNode(retrySetting)); + + Map configs = new LinkedHashMap<>(); + configs.put("port", new LeafNode("100")); + configs.put("uri", new LeafNode("300")); + configs.put("settings", new MapNode(settings)); + + SequencedMapDecoder decoder = new SequencedMapDecoder(); + + ValidateOf> validate = decoder.decode("db.host", Tags.of(), new MapNode(configs), + new TypeCapture>() { }, new DecoderContext(decoderService, null)); + Assertions.assertTrue(validate.hasResults()); + Assertions.assertFalse(validate.hasErrors()); + + Map results = (Map) validate.results(); + Assertions.assertEquals(100, results.get("port")); + Assertions.assertEquals(300, results.get("uri")); + Assertions.assertEquals(123, results.get("settings.timeout")); + Assertions.assertEquals(2, results.get("settings.retry.times")); + Assertions.assertEquals(7, results.get("settings.retry.delay")); + + SequencedSet> results2 = ((SequencedMap) validate.results()).sequencedEntrySet(); + Assertions.assertEquals(5, results2.size()); + Assertions.assertEquals(Map.entry("port", 100 ), results2.removeFirst()); + Assertions.assertEquals(Map.entry("uri", 300 ), results2.removeFirst()); + Assertions.assertEquals(Map.entry("settings.timeout", 123), results2.removeFirst()); + Assertions.assertEquals(Map.entry("settings.retry.times", 2), results2.removeFirst()); + Assertions.assertEquals(Map.entry("settings.retry.delay", 7), results2.removeFirst()); + } + + @Test + void decodeNestedMapWithArray() { + List retrySetting = new ArrayList<>(); + retrySetting.add(new LeafNode("2")); + retrySetting.add(new LeafNode("7")); + + Map settings = new LinkedHashMap<>(); + settings.put("timeout", new LeafNode("123")); + settings.put("retryList", new ArrayNode(retrySetting)); + + Map configs = new LinkedHashMap<>(); + configs.put("port", new LeafNode("100")); + configs.put("uri", new LeafNode("300")); + configs.put("settings", new MapNode(settings)); + + SequencedMapDecoder decoder = new SequencedMapDecoder(); + + ValidateOf> validate = decoder.decode("db.host", Tags.of(), new MapNode(configs), + new TypeCapture>() { }, new DecoderContext(decoderService, null)); + Assertions.assertTrue(validate.hasResults()); + Assertions.assertFalse(validate.hasErrors()); + + Map results = (Map) validate.results(); + Assertions.assertEquals(100, results.get("port")); + Assertions.assertEquals(300, results.get("uri")); + Assertions.assertEquals(123, results.get("settings.timeout")); + Assertions.assertEquals(2, results.get("settings.retryList[0]")); + Assertions.assertEquals(7, results.get("settings.retryList[1]")); + + SequencedSet> results2 = ((SequencedMap) validate.results()).sequencedEntrySet(); + Assertions.assertEquals(5, results2.size()); + Assertions.assertEquals(Map.entry("port", 100), results2.removeFirst()); + Assertions.assertEquals(Map.entry("uri", 300), results2.removeFirst()); + Assertions.assertEquals(Map.entry( "settings.timeout", 123), results2.removeFirst()); + Assertions.assertEquals(Map.entry("settings.retryList[0]", 2), results2.removeFirst()); + Assertions.assertEquals(Map.entry("settings.retryList[1]", 7), results2.removeFirst()); + } + + @Test + void decodeNullLeafNodeValue() { + SequencedMapDecoder decoder = new SequencedMapDecoder(); + + Map configs = new LinkedHashMap<>(); + configs.put("port", new LeafNode(null)); + configs.put("uri", new LeafNode("mysql.com")); + configs.put("password", new LeafNode("pass")); + + ValidateOf> validate = decoder.decode("db.host", Tags.of(), + new MapNode(configs), new TypeCapture>() { }, new DecoderContext(decoderService, null)); + Assertions.assertTrue(validate.hasResults()); + Assertions.assertTrue(validate.hasErrors()); + + Assertions.assertEquals(2, validate.getErrors().size()); + Assertions.assertEquals("Leaf on path: db.host.port, has no value attempting to decode String", + validate.getErrors().get(0).description()); + Assertions.assertEquals("Map key was null on path: db.host.port", + validate.getErrors().get(1).description()); + + Map results = (Map) validate.results(); + Assertions.assertNull(results.get("port")); + Assertions.assertEquals("mysql.com", results.get("uri")); + Assertions.assertEquals("pass", results.get("password")); + + SequencedSet> results2 = ((SequencedMap) validate.results()).sequencedEntrySet(); + Assertions.assertEquals(3, results2.size()); + var first = results2.removeFirst(); + Assertions.assertEquals("port", first.getKey()); + Assertions.assertEquals(null, first.getValue()); + Assertions.assertEquals(Map.entry("uri", "mysql.com"), results2.removeFirst()); + Assertions.assertEquals(Map.entry("password", "pass"), results2.removeFirst()); + } + + @Test + void decodeNullKeyNodeValue() { + SequencedMapDecoder decoder = new SequencedMapDecoder(); + + Map configs = new LinkedHashMap<>(); + configs.put(null, new LeafNode("100")); + configs.put("uri", new LeafNode("mysql.com")); + configs.put("password", new LeafNode("pass")); + + ValidateOf> validate = decoder.decode("db.host", Tags.of(), + new MapNode(configs), new TypeCapture>() { }, new DecoderContext(decoderService, null)); + Assertions.assertTrue(validate.hasResults()); + Assertions.assertTrue(validate.hasErrors()); + + Assertions.assertEquals(1, validate.getErrors().size()); + Assertions.assertEquals("Map key was null on path: db.host", + validate.getErrors().get(0).description()); + + Map results = (Map) validate.results(); + Assertions.assertNull(results.get("port")); + Assertions.assertEquals("mysql.com", results.get("uri")); + Assertions.assertEquals("pass", results.get("password")); + + SequencedSet> results2 = ((SequencedMap) validate.results()).sequencedEntrySet(); + Assertions.assertEquals(2, results2.size()); + Assertions.assertEquals(Map.entry("uri", "mysql.com"), results2.removeFirst()); + Assertions.assertEquals(Map.entry("password", "pass"), results2.removeFirst()); + } + + @Test + void decodeNullLeafNode() { + SequencedMapDecoder decoder = new SequencedMapDecoder(); + + Map configs = new LinkedHashMap<>(); + configs.put("port", null); + configs.put("uri", new LeafNode("mysql.com")); + configs.put("password", new LeafNode("pass")); + + ValidateOf> validate = decoder.decode("db.host", Tags.of(), + new MapNode(configs), new TypeCapture>() { }, new DecoderContext(decoderService, null)); + Assertions.assertTrue(validate.hasResults()); + Assertions.assertTrue(validate.hasErrors()); + + Assertions.assertEquals(2, validate.getErrors().size()); + Assertions.assertEquals("Expected a leaf on path: db.host.port, received node type: null, attempting to decode String", + validate.getErrors().get(0).description()); + Assertions.assertEquals("Map key was null on path: db.host.port", + validate.getErrors().get(1).description()); + + Map results = (Map) validate.results(); + Assertions.assertNull(results.get("port")); + Assertions.assertEquals("mysql.com", results.get("uri")); + Assertions.assertEquals("pass", results.get("password")); + + SequencedSet> results2 = ((SequencedMap) validate.results()).sequencedEntrySet(); + Assertions.assertEquals(3, results2.size()); + var first = results2.removeFirst(); + Assertions.assertEquals("port", first.getKey()); + Assertions.assertEquals(null, first.getValue()); + Assertions.assertEquals(Map.entry("uri", "mysql.com"), results2.removeFirst()); + Assertions.assertEquals(Map.entry("password", "pass"), results2.removeFirst()); + } + + @Test + void decodeWrongNodeType() { + SequencedMapDecoder decoder = new SequencedMapDecoder(); + + ValidateOf> validate = decoder.decode("db.host", Tags.of(), + new LeafNode("mysql.com"), new TypeCapture>() { }, new DecoderContext(decoderService, null)); + Assertions.assertFalse(validate.hasResults()); + Assertions.assertTrue(validate.hasErrors()); + + Assertions.assertEquals(1, validate.getErrors().size()); + Assertions.assertEquals("Expected a map node on path: db.host, received node type : LEAF", + validate.getErrors().get(0).description()); + } + + @Test + void decodeWrongType() { + + Map configs = new LinkedHashMap<>(); + configs.put("port", new LeafNode("100")); + configs.put("uri", new LeafNode("300")); + configs.put("password", new LeafNode("6000")); + + SequencedMapDecoder decoder = new SequencedMapDecoder(); + + ValidateOf> validate = decoder.decode("db.host", Tags.of(), new MapNode(configs), + new TypeCapture>() { }, new DecoderContext(decoderService, null)); + Assertions.assertFalse(validate.hasResults()); + Assertions.assertTrue(validate.hasErrors()); + + Assertions.assertEquals(1, validate.getErrors().size()); + Assertions.assertEquals("Expected a map on path: db.host, received node type : map, " + + "received invalid types: [TypeCapture{rawType=class java.lang.String, type=class java.lang.String}]", + validate.getErrors().get(0).description()); + } + + @Test + void decodeMapNodeNullInside() { + SequencedMapDecoder decoder = new SequencedMapDecoder(); + + ValidateOf> validate = decoder.decode("db.host", Tags.of(), new MapNode(null), + new TypeCapture>() { }, new DecoderContext(decoderService, null)); + Assertions.assertFalse(validate.hasResults()); + Assertions.assertTrue(validate.hasErrors()); + + Assertions.assertEquals(1, validate.getErrors().size()); + Assertions.assertEquals("Expected a map on path: db.host, received node type : map, " + + "received invalid types: [TypeCapture{rawType=class java.lang.String, type=class java.lang.String}]", + validate.getErrors().get(0).description()); + } + + @Test + void decodeNullNode() { + SequencedMapDecoder decoder = new SequencedMapDecoder(); + ValidateOf> validate = decoder.decode("db.host", Tags.of(), null, new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + Assertions.assertFalse(validate.hasResults()); + Assertions.assertTrue(validate.hasErrors()); + + Assertions.assertEquals(1, validate.getErrors().size()); + Assertions.assertEquals("Expected a map node on path: db.host, received node type : null", + validate.getErrors().get(0).description()); + } + + static class User { + + private String name; + private Integer age; + + public User() { + + } + + public User(String name, Integer age) { + this.name = name; + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + User user = (User) o; + return Objects.equals(name, user.name) && Objects.equals(age, user.age); + } + + @Override + public int hashCode() { + return Objects.hash(name, age); + } + } + +} diff --git a/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedSetDecoderTest.java b/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedSetDecoderTest.java new file mode 100644 index 000000000..2d18869f5 --- /dev/null +++ b/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedSetDecoderTest.java @@ -0,0 +1,380 @@ +package org.github.gestalt.config.decoder; + +import org.github.gestalt.config.exceptions.GestaltConfigurationException; +import org.github.gestalt.config.lexer.SentenceLexer; +import org.github.gestalt.config.node.*; +import org.github.gestalt.config.path.mapper.StandardPathMapper; +import org.github.gestalt.config.reflect.TypeCapture; +import org.github.gestalt.config.tag.Tags; +import org.github.gestalt.config.utils.ValidateOf; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.util.*; + +import static org.assertj.core.api.Assertions.assertThat; + +class SequencedSetDecoderTest { + final DoubleDecoder doubleDecoder = new DoubleDecoder(); + final StringDecoder stringDecoder = new StringDecoder(); + final ListDecoder listDecoder = new ListDecoder(); + + final SetDecoder setDecoder = new SetDecoder(); + + final SequencedSetDecoder sequencedSetDecoder = new SequencedSetDecoder(); + + ConfigNodeService configNodeService; + DecoderRegistry decoderService; + SentenceLexer lexer; + + @BeforeEach + void setup() throws GestaltConfigurationException { + configNodeService = Mockito.mock(ConfigNodeService.class); + lexer = Mockito.mock(SentenceLexer.class); + decoderService = new DecoderRegistry(List.of(doubleDecoder, stringDecoder, sequencedSetDecoder, listDecoder, setDecoder), configNodeService, lexer, + List.of(new StandardPathMapper())); + } + + @Test + void name() { + SequencedSetDecoder decoder = new SequencedSetDecoder(); + Assertions.assertEquals("SequencedSet", decoder.name()); + } + + @Test + void priority() { + SequencedSetDecoder decoder = new SequencedSetDecoder(); + Assertions.assertEquals(Priority.HIGH, decoder.priority()); + } + + @Test + void canDecode() { + SequencedSetDecoder decoder = new SequencedSetDecoder(); + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), TypeCapture.of(String.class))); + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture() { + })); + + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), TypeCapture.of(Long.class))); + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture() { + })); + + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), TypeCapture.of(List.class))); + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture>() { + })); + + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), TypeCapture.of(Set.class))); + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture>() { + })); + + Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture>() { + })); + + Assertions.assertTrue(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture>() { + })); + + Assertions.assertTrue(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture>() { + })); + } + + @Test + @SuppressWarnings("unchecked") + void arrayDecodeStrings() { + + ConfigNode[] arrayNode = new ConfigNode[3]; + arrayNode[0] = new LeafNode("John"); + arrayNode[1] = new LeafNode("Steve"); + arrayNode[2] = new LeafNode("Matt"); + + ConfigNode nodes = new ArrayNode(Arrays.asList(arrayNode)); + SequencedSetDecoder decoder = new SequencedSetDecoder(); + + ValidateOf> values = decoder.decode("", Tags.of(), nodes, new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(3, values.results().size()); + Set results = (Set) values.results(); + + assertThat(results) + .contains("John") + .contains("Steve") + .contains("Matt"); + + SequencedSet results2 = (SequencedSet) values.results(); + + + Assertions.assertEquals("John", results2.removeFirst()); + Assertions.assertEquals("Steve", results2.removeFirst()); + Assertions.assertEquals("Matt", results2.removeFirst()); + Assertions.assertTrue(results2.isEmpty()); + } + + @Test + @SuppressWarnings("unchecked") + void arrayDecodeDoubles() { + + ConfigNode[] arrayNode = new ConfigNode[3]; + arrayNode[0] = new LeafNode("0.1111"); + arrayNode[1] = new LeafNode("0.222"); + arrayNode[2] = new LeafNode("0.33"); + + ConfigNode nodes = new ArrayNode(Arrays.asList(arrayNode)); + SequencedSetDecoder decoder = new SequencedSetDecoder(); + + ValidateOf> values = decoder.decode("", Tags.of(), nodes, new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(3, values.results().size()); + + Set results = (Set) values.results(); + assertThat(results) + .contains(0.1111) + .contains(0.222) + .contains(0.33); + + SequencedSet results2 = (SequencedSet) values.results(); + Assertions.assertEquals(0.1111, results2.removeFirst()); + Assertions.assertEquals(0.222, results2.removeFirst()); + Assertions.assertEquals(0.33, results2.removeFirst()); + } + + @Test + @SuppressWarnings("unchecked") + void arrayDecodeDoublesMissingIndex() { + + ConfigNode[] arrayNode = new ConfigNode[4]; + arrayNode[0] = new LeafNode("0.1111"); + arrayNode[1] = new LeafNode("0.222"); + arrayNode[3] = new LeafNode("0.33"); + + ConfigNode nodes = new ArrayNode(Arrays.asList(arrayNode)); + SequencedSetDecoder decoder = new SequencedSetDecoder(); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), nodes, new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertTrue(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(3, values.results().size()); + Assertions.assertEquals(1, values.getErrors().size()); + Assertions.assertEquals("Missing array index: 2 for path: db.hosts", values.getErrors().get(0).description()); + + Set results = (Set) values.results(); + assertThat(results) + .contains(0.1111) + .contains(0.222) + .contains(0.33); + + SequencedSet results2 = (SequencedSet) values.results(); + Assertions.assertEquals(0.1111, results2.removeFirst()); + Assertions.assertEquals(0.222, results2.removeFirst()); + Assertions.assertEquals(0.33, results2.removeFirst()); + } + + @Test + @SuppressWarnings("unchecked") + void arrayDecodeLeaf() { + SequencedSetDecoder decoder = new SequencedSetDecoder(); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), new LeafNode("0.1111, 0.22"), new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + + Assertions.assertEquals(2, values.results().size()); + Set results = (Set) values.results(); + assertThat(results) + .contains(0.1111) + .contains(0.22); + + SequencedSet results2 = (SequencedSet) values.results(); + Assertions.assertEquals(0.1111, results2.removeFirst()); + Assertions.assertEquals(0.22, results2.removeFirst()); + } + + @Test + @SuppressWarnings("unchecked") + void arrayDecodeLeafWithEscapeComma() { + SequencedSetDecoder decoder = new SequencedSetDecoder(); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), new LeafNode("a,b,c\\,d"), new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertFalse(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + + Assertions.assertEquals(3, values.results().size()); + Set results = (Set) values.results(); + assertThat(results) + .contains("a") + .contains("b") + .contains("c,d"); + + SequencedSet results2 = (SequencedSet) values.results(); + Assertions.assertEquals("a", results2.removeFirst()); + Assertions.assertEquals("b", results2.removeFirst()); + Assertions.assertEquals("c,d", results2.removeFirst()); + } + + @Test + void arrayDecodeNullLeaf() { + SequencedSetDecoder decoder = new SequencedSetDecoder(); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), new LeafNode(null), new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertTrue(values.hasErrors()); + Assertions.assertFalse(values.hasResults()); + + Assertions.assertEquals(1, values.getErrors().size()); + Assertions.assertEquals("Leaf on path: db.hosts, has no value attempting to decode SequencedSet", + values.getErrors().get(0).description()); + } + + @Test + void arrayDecodeNullNode() { + SequencedSetDecoder decoder = new SequencedSetDecoder(); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), null, new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertTrue(values.hasErrors()); + Assertions.assertFalse(values.hasResults()); + + Assertions.assertEquals(1, values.getErrors().size()); + Assertions.assertEquals("Expected a Array on path: db.hosts, received node type: null, attempting to decode SequencedSet", + values.getErrors().get(0).description()); + } + + + @Test + void arrayDecodeEmptyArrayNodeOk() { + ConfigNode nodes = new ArrayNode(List.of()); + SequencedSetDecoder decoder = new SequencedSetDecoder(); + + 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); + SequencedSetDecoder decoder = new SequencedSetDecoder(); + + 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(""); + SequencedSetDecoder decoder = new SequencedSetDecoder(); + + 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("")); + + Assertions.assertEquals(1, values.results().size()); + Assertions.assertTrue(values.results().contains("")); + + SequencedSet results2 = (SequencedSet) values.results(); + Assertions.assertEquals("", results2.removeFirst()); + } + + @Test + void arrayDecodeWrongTypeDoubles() { + + ConfigNode[] arrayNode = new ConfigNode[3]; + arrayNode[0] = new LeafNode("John"); + arrayNode[1] = new LeafNode("Matt"); + arrayNode[2] = new LeafNode("Tom"); + + ConfigNode nodes = new ArrayNode(Arrays.asList(arrayNode)); + SequencedSetDecoder decoder = new SequencedSetDecoder(); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), nodes, new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertTrue(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + Assertions.assertEquals(0, values.results().size()); + + Assertions.assertEquals(3, values.getErrors().size()); + Assertions.assertEquals("Unable to parse a number on Path: db.hosts[0], from node: LeafNode{value='John'} " + + "attempting to decode Double", + values.getErrors().get(0).description()); + Assertions.assertEquals("Unable to parse a number on Path: db.hosts[1], from node: LeafNode{value='Matt'} " + + "attempting to decode Double", + values.getErrors().get(1).description()); + Assertions.assertEquals("Unable to parse a number on Path: db.hosts[2], from node: LeafNode{value='Tom'} " + + "attempting to decode Double", + values.getErrors().get(2).description()); + } + + @Test + @SuppressWarnings("unchecked") + void arrayDecodeMixedWrongTypeDoubles() { + + ConfigNode[] arrayNode = new ConfigNode[3]; + arrayNode[0] = new LeafNode("John"); + arrayNode[1] = new LeafNode("0.22"); + arrayNode[2] = new LeafNode("Tom"); + + ConfigNode nodes = new ArrayNode(Arrays.asList(arrayNode)); + SequencedSetDecoder decoder = new SequencedSetDecoder(); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), nodes, new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertTrue(values.hasErrors()); + Assertions.assertTrue(values.hasResults()); + + Assertions.assertEquals(2, values.getErrors().size()); + Assertions.assertEquals("Unable to parse a number on Path: db.hosts[0], from node: LeafNode{value='John'} " + + "attempting to decode Double", + values.getErrors().get(0).description()); + Assertions.assertEquals("Unable to parse a number on Path: db.hosts[2], from node: LeafNode{value='Tom'} " + + "attempting to decode Double", + values.getErrors().get(1).description()); + + Set results = (Set) values.results(); + assertThat(results) + .contains(0.22); + + SequencedSet results2 = (SequencedSet) values.results(); + Assertions.assertEquals(0.22, results2.removeFirst()); + } + + @Test + void arrayDecodeMapNode() { + SequencedSetDecoder decoder = new SequencedSetDecoder(); + + ValidateOf> values = decoder.decode("db.hosts", Tags.of(), new MapNode(new HashMap<>()), new TypeCapture>() { + }, new DecoderContext(decoderService, null)); + + Assertions.assertTrue(values.hasErrors()); + Assertions.assertFalse(values.hasResults()); + + Assertions.assertEquals(1, values.getErrors().size()); + Assertions.assertEquals("Expected a Array on path: db.hosts, received node type: MAP, attempting to decode SequencedSet", + values.getErrors().get(0).description()); + } +} diff --git a/gestalt-test/src/test/resources/default.properties b/gestalt-test/src/test/resources/default.properties new file mode 100644 index 000000000..9804c1f23 --- /dev/null +++ b/gestalt-test/src/test/resources/default.properties @@ -0,0 +1,34 @@ +DB.hosts[0].user=credmond +DB.hosts[0].url=jdbc:postgresql://localhost:5432/mydb1 +DB.hosts[1].user=credmond +DB.hosts[1].url=jdbc:postgresql://localhost:5432/mydb2 +DB.hosts[2].user=credmond +db.hosts[2].url=jdbc:postgresql://localhost:5432/mydb3 +db.ConnectionTimeout=6000 +db.idle-timeout=600 +db.max_lifetime=60000.0 + +http.pool.maxTotal=100 +http.pool.maxPerRoute=10 +http.pool.validateAfterInactivity=6000 +http.pool.keepAliveTimeoutMs=60000 +http.pool.idleTimeoutSec=25 + +subservice.booking.service.isEnabled=false +subservice.booking.service.host=http://localhost +subservice.booking.service.port=8081 +subservice.booking.service.path=booking + +subservice.search.service.isEnabled=false +subservice.search.service.host=http://localhost +subservice.search.service.port=8082 +subservice.search.service.path=search + +ADMIN.user=John, Sarah +ADMIN.overrideEnabled=false +ADMIN.accessRole=level0 + +employee.user=Janice +employee.accessRole=level1 + +serviceMode=active diff --git a/gestalt-test/src/test/resources/dev.properties b/gestalt-test/src/test/resources/dev.properties new file mode 100644 index 000000000..bfb57e005 --- /dev/null +++ b/gestalt-test/src/test/resources/dev.properties @@ -0,0 +1,18 @@ +db.hosts[0].url=jdbc:postgresql://dev.host.name1:5432/mydb +db.hosts[1].url=jdbc:postgresql://dev.host.name2:5432/mydb +db.hosts[2].url=jdbc:postgresql://dev.host.name3:5432/mydb +db.connectionTimeout=600 + +http.pool.maxTotal=1000 +http.pool.maxPerRoute=50 + +subservice.booking.isEnabled=true +subservice.booking.service.host=https://dev.bank.host.name +subservice.booking.service.port=443 +subservice.booking.service.path=booking + +subservice.search.service.isEnabled=false + +admin.user=Peter, Kim, Steve +admin.overrideEnabled=true + From 2b1b26ec49e8cb5f01fcf5661f4401d957869a36 Mon Sep 17 00:00:00 2001 From: Colin Redmond Date: Wed, 3 Jan 2024 16:04:45 -0800 Subject: [PATCH 2/9] fix: spelling Error. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 82bec8361..4aa6a5612 100644 --- a/README.md +++ b/README.md @@ -765,7 +765,7 @@ To register your own default ConfigLoaders add them to the builder, or add it to | Object | Decodes a java Bean style class, although it will work with any java class. Will fail if the constructor is private. Will construct the class even if there are missing values, the values will be null or the default. Then it will return errors which you can disable using treatMissingValuesAsErrors = true. Decodes member classes and lists as well. | | Optional | Decodes an optional value, if no value is found it will return an Optional.empty() | | OptionalDouble | Decodes an optional Double, if no value is found it will return an OptionalDouble.empty() | -| OptionaInt | Decodes an optional Integer, if no value is found it will return an OptionaInt.empty() | +| OptionalInt | Decodes an optional Integer, if no value is found it will return an OptionaInt.empty() | | OptionalLong | Decodes an optional Long, if no value is found it will return an OptionalLong.empty() | | Path | | | Pattern | | From 0d89ae0c1d14e8c7a85209c33b44baede458d7d6 Mon Sep 17 00:00:00 2001 From: Colin Redmond Date: Wed, 3 Jan 2024 16:11:23 -0800 Subject: [PATCH 3/9] fix: correct name of SequencedCollection Decoder --- .../config/decoder/SequencedCollectionDecoderTest.java | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedCollectionDecoderTest.java b/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedCollectionDecoderTest.java index 18f3abee8..0bcba1fa8 100644 --- a/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedCollectionDecoderTest.java +++ b/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedCollectionDecoderTest.java @@ -37,7 +37,7 @@ void setup() throws GestaltConfigurationException { void name() { SequencedCollectionDecoder decoder = new SequencedCollectionDecoder(); - Assertions.assertEquals("SequencedList", decoder.name()); + Assertions.assertEquals("SequencedCollection", decoder.name()); } @Test @@ -215,7 +215,7 @@ void arrayDecodeNullLeaf() { Assertions.assertFalse(values.hasResults()); Assertions.assertEquals(1, values.getErrors().size()); - Assertions.assertEquals("Leaf on path: db.hosts, has no value attempting to decode SequencedList", + Assertions.assertEquals("Leaf on path: db.hosts, has no value attempting to decode SequencedCollection", values.getErrors().get(0).description()); } @@ -230,7 +230,7 @@ void arrayDecodeNullNode() { Assertions.assertFalse(values.hasResults()); Assertions.assertEquals(1, values.getErrors().size()); - Assertions.assertEquals("Expected a Array on path: db.hosts, received node type: null, attempting to decode SequencedList", + Assertions.assertEquals("Expected a Array on path: db.hosts, received node type: null, attempting to decode SequencedCollection", values.getErrors().get(0).description()); } @@ -348,7 +348,7 @@ void arrayDecodeMapNode() { Assertions.assertFalse(values.hasResults()); Assertions.assertEquals(1, values.getErrors().size()); - Assertions.assertEquals("Expected a Array on path: db.hosts, received node type: MAP, attempting to decode SequencedList", + Assertions.assertEquals("Expected a Array on path: db.hosts, received node type: MAP, attempting to decode SequencedCollection", values.getErrors().get(0).description()); } } From c07b530bb60dd461de960e48d4b3f137320253e1 Mon Sep 17 00:00:00 2001 From: Colin Redmond Date: Wed, 3 Jan 2024 16:15:21 -0800 Subject: [PATCH 4/9] feat: update Dependencies. --- gradle/libs.versions.toml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cd89dfb65..a50649b04 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,10 +7,9 @@ kotlin = "1.9.21" kotlinDokka = "1.9.10" # Kotlin DI kodeinDI = "7.21.1" -koinDI = "3.5.0" +koinDI = "3.5.3" # Java DI -# @pin -guice = "6.0.0" +guice = "7.0.0" # @pin cdi = "3.0.0" # @pin @@ -18,11 +17,11 @@ weld = "3.1.0.Final" # @pin weldCore = "4.0.3.Final" #encoding/decoding -jackson = "2.16.0" +jackson = "2.16.1" hocon = "1.4.3" # Cloud -awsBom = "2.21.39" -gcpLibraries = "26.28.0" +awsBom = "2.22.10" +gcpLibraries = "26.29.0" # vault vault = "6.2.0" # Git support @@ -30,7 +29,7 @@ jgit = "6.8.0.202311291450-r" eddsa = "0.3.0" # testing junit5 = "5.10.1" -assertJ = "3.24.2" +assertJ = "3.25.1" mockito = "5.2.0" mockk = "1.13.8" koTestAssertions = "5.8.0" @@ -38,7 +37,7 @@ koTestAssertions = "5.8.0" awsMock = "2.17.0" testcontainers = "1.19.3" # static code analysis -errorprone = "2.23.0" +errorprone = "2.24.1" gradleErrorProne = "3.1.0" detekt = "1.23.4" checkStyle = "10.10.0" @@ -50,7 +49,7 @@ gestalt = "0.24.2" # Gradle utility gradleVersions = "0.50.0" gitVersions = "3.0.0" -gradleVersionsUpdate = "0.8.1" +gradleVersionsUpdate = "0.8.3" [libraries] # Kotlin From ea32cc6e5938f2d9e66a0c5e74c46b9f0acaa3bd Mon Sep 17 00:00:00 2001 From: Colin Redmond Date: Wed, 3 Jan 2024 16:31:22 -0800 Subject: [PATCH 5/9] fix: remove mockito from Sequenced tests. --- .../config/decoder/SequencedCollectionDecoderTest.java | 5 +++-- .../gestalt/config/decoder/SequencedSetDecoderTest.java | 5 +++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedCollectionDecoderTest.java b/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedCollectionDecoderTest.java index 0bcba1fa8..4864ec293 100644 --- a/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedCollectionDecoderTest.java +++ b/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedCollectionDecoderTest.java @@ -1,6 +1,7 @@ package org.github.gestalt.config.decoder; import org.github.gestalt.config.exceptions.GestaltConfigurationException; +import org.github.gestalt.config.lexer.PathLexer; import org.github.gestalt.config.lexer.SentenceLexer; import org.github.gestalt.config.node.*; import org.github.gestalt.config.path.mapper.StandardPathMapper; @@ -27,8 +28,8 @@ class SequencedCollectionDecoderTest { @BeforeEach void setup() throws GestaltConfigurationException { - configNodeService = Mockito.mock(ConfigNodeService.class); - lexer = Mockito.mock(SentenceLexer.class); + configNodeService = new ConfigNodeManager(); + lexer = new PathLexer(); decoderService = new DecoderRegistry(List.of(doubleDecoder, stringDecoder, SequencedCollectionDecoder, listDecoder), configNodeService, lexer, List.of(new StandardPathMapper())); } diff --git a/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedSetDecoderTest.java b/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedSetDecoderTest.java index 2d18869f5..d0163836c 100644 --- a/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedSetDecoderTest.java +++ b/gestalt-test/src/test/java/org/github/gestalt/config/decoder/SequencedSetDecoderTest.java @@ -1,6 +1,7 @@ package org.github.gestalt.config.decoder; import org.github.gestalt.config.exceptions.GestaltConfigurationException; +import org.github.gestalt.config.lexer.PathLexer; import org.github.gestalt.config.lexer.SentenceLexer; import org.github.gestalt.config.node.*; import org.github.gestalt.config.path.mapper.StandardPathMapper; @@ -31,8 +32,8 @@ class SequencedSetDecoderTest { @BeforeEach void setup() throws GestaltConfigurationException { - configNodeService = Mockito.mock(ConfigNodeService.class); - lexer = Mockito.mock(SentenceLexer.class); + configNodeService = new ConfigNodeManager(); + lexer = new PathLexer(); decoderService = new DecoderRegistry(List.of(doubleDecoder, stringDecoder, sequencedSetDecoder, listDecoder, setDecoder), configNodeService, lexer, List.of(new StandardPathMapper())); } From 6d21b687dbe90380232887085970016e49704187 Mon Sep 17 00:00:00 2001 From: Colin Redmond Date: Wed, 3 Jan 2024 16:55:01 -0800 Subject: [PATCH 6/9] feat: convert tests to test suite. --- .../gestalt.java-test-conventions.gradle.kts | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/buildSrc/src/main/kotlin/gestalt.java-test-conventions.gradle.kts b/buildSrc/src/main/kotlin/gestalt.java-test-conventions.gradle.kts index 2f2623ebd..c4e14b3a9 100644 --- a/buildSrc/src/main/kotlin/gestalt.java-test-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/gestalt.java-test-conventions.gradle.kts @@ -6,6 +6,7 @@ plugins { id("gestalt.java-common-conventions") + `jvm-test-suite` jacoco } @@ -25,11 +26,19 @@ tasks.jacocoTestReport { dependsOn(tasks.test) // tests are required to run before generating the report } -tasks.test { - // Use junit platform for unit tests - systemProperty("junit.jupiter.execution.parallel.enabled", "true") - useJUnitPlatform() - finalizedBy(tasks.jacocoTestReport) +testing { + suites { + val test by getting(JvmTestSuite::class) { + useJUnitJupiter() + targets { + all { + testTask { + finalizedBy(tasks.jacocoTestReport) + } + } + } + } + } } tasks.jacocoTestReport { From 79e937d94223350c4518597143497c34010e6f7a Mon Sep 17 00:00:00 2001 From: Colin Redmond Date: Wed, 3 Jan 2024 16:57:24 -0800 Subject: [PATCH 7/9] feat: specify the version of junit. --- .../src/main/kotlin/gestalt.java-test-conventions.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/buildSrc/src/main/kotlin/gestalt.java-test-conventions.gradle.kts b/buildSrc/src/main/kotlin/gestalt.java-test-conventions.gradle.kts index c4e14b3a9..4f2b4d246 100644 --- a/buildSrc/src/main/kotlin/gestalt.java-test-conventions.gradle.kts +++ b/buildSrc/src/main/kotlin/gestalt.java-test-conventions.gradle.kts @@ -29,7 +29,7 @@ tasks.jacocoTestReport { testing { suites { val test by getting(JvmTestSuite::class) { - useJUnitJupiter() + useJUnitJupiter(libs.versions.junit5.get()) targets { all { testTask { From 2f685bb69c0cbcabeab91148decdb30315a30ce6 Mon Sep 17 00:00:00 2001 From: Colin Redmond Date: Wed, 3 Jan 2024 17:19:19 -0800 Subject: [PATCH 8/9] feat: get code coverage data from code-coverage-report directory. --- .github/workflows/gradle.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index e45c38376..f82a1d05f 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -45,5 +45,7 @@ jobs: - name: Publish Coverage if: success() uses: codecov/codecov-action@v3 - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + with: + token: ${{ secrets.CODECOV_TOKEN }} + directory: ./code-coverage-report/build/reports/jacoco/testCodeCoverageReport + fail_ci_if_error: true From 3fd1f5e2a90ee194060b3803fdb5964ca8401e62 Mon Sep 17 00:00:00 2001 From: Colin Redmond Date: Wed, 3 Jan 2024 17:28:25 -0800 Subject: [PATCH 9/9] feat: try 2 for code coverage. --- .github/workflows/gradle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index f82a1d05f..09975152e 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -47,5 +47,5 @@ jobs: uses: codecov/codecov-action@v3 with: token: ${{ secrets.CODECOV_TOKEN }} - directory: ./code-coverage-report/build/reports/jacoco/testCodeCoverageReport + files: ./code-coverage-report/build/reports/jacoco/testCodeCoverageReport/testCodeCoverageReport.xml fail_ci_if_error: true