Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/sequenced #135

Merged
merged 9 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/gradle.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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 }}
files: ./code-coverage-report/build/reports/jacoco/testCodeCoverageReport/testCodeCoverageReport.xml
fail_ci_if_error: true
75 changes: 39 additions & 36 deletions README.md

Large diffs are not rendered by default.

19 changes: 14 additions & 5 deletions buildSrc/src/main/kotlin/gestalt.java-test-conventions.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

plugins {
id("gestalt.java-common-conventions")
`jvm-test-suite`
jacoco
}

Expand All @@ -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(libs.versions.junit5.get())
targets {
all {
testTask {
finalizedBy(tasks.jacocoTestReport)
}
}
}
}
}
}

tasks.jacocoTestReport {
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <a href="mailto:colin.redmond@outlook.com"> Colin Redmond </a> (c) 2023.
*/
public final class SequencedCollectionDecoder extends CollectionDecoder<List<?>> {

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<List<?>> arrayDecode(String path, Tags tags, ConfigNode node, TypeCapture<?> klass,
DecoderContext decoderContext) {
List<ValidationError> errors = new ArrayList<>();
List<Object> 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);
}
}
Original file line number Diff line number Diff line change
@@ -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 <a href="mailto:colin.redmond@outlook.com"> Colin Redmond </a> (c) 2023.
*/
public final class SequencedMapDecoder implements Decoder<Map<?, ?>> {

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<Map<?, ?>> decode(String path, Tags tags, ConfigNode node, TypeCapture<?> type, DecoderContext decoderContext) {
ValidateOf<Map<?, ?>> results;
if (node instanceof MapNode) {
MapNode mapNode = (MapNode) node;
List<TypeCapture<?>> 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<ValidationError> 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<Object> keyValidate = decoderContext.getDecoderService()
.decodeNode(nextPath, tags, new LeafNode(key), (TypeCapture<Object>) keyType, decoderContext);
ValidateOf<Object> valueValidate = decoderContext.getDecoderService()
.decodeNode(nextPath, tags, it.getValue(), (TypeCapture<Object>) valueType, decoderContext);

errors.addAll(keyValidate.getErrors());
errors.addAll(valueValidate.getErrors());

if (!keyValidate.hasResults()) {
errors.add(new ValidationError.DecodersMapKeyNull(nextPath));

Check warning on line 94 in gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedMapDecoder.java

View check run for this annotation

Codecov / codecov/patch

gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedMapDecoder.java#L94

Added line #L94 was not covered by tests
}
if (!valueValidate.hasResults()) {
errors.add(new ValidationError.DecodersMapValueNull(nextPath));
}

if (keyValidate.hasResults()) {
return new Pair<>(keyValidate.results(), valueValidate.results());
}
return null;

Check warning on line 103 in gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedMapDecoder.java

View check run for this annotation

Codecov / codecov/patch

gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedMapDecoder.java#L103

Added line #L103 was not covered by tests
})
.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<Map.Entry<String, ConfigNode>> convertMapToStream(String path, Map.Entry<String, ConfigNode> 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);

Check warning on line 120 in gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedMapDecoder.java

View check run for this annotation

Codecov / codecov/patch

gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedMapDecoder.java#L120

Added line #L120 was not covered by tests
} 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<Map.Entry<String, ConfigNode>> stream = Stream.of();
List<ConfigNode> 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();

Check warning on line 139 in gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedMapDecoder.java

View check run for this annotation

Codecov / codecov/patch

gestalt-core/src/main/java/org/github/gestalt/config/decoder/SequencedMapDecoder.java#L139

Added line #L139 was not covered by tests
}
}
}
Original file line number Diff line number Diff line change
@@ -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 <a href="mailto:colin.redmond@outlook.com"> Colin Redmond </a> (c) 2023.
*/
public final class SequencedSetDecoder extends CollectionDecoder<Set<?>> {

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<Set<?>> arrayDecode(String path, Tags tags, ConfigNode node, TypeCapture<?> klass, DecoderContext decoderContext) {
List<ValidationError> errors = new ArrayList<>();
Set<Object> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ private ValidateOf<ConfigNode> buildArrayConfigTree(String path, ConfigList conf

private ValidateOf<ConfigNode> buildObjectConfigTree(String path, ConfigObject configObject) {
List<ValidationError> errors = new ArrayList<>();
Map<String, ConfigNode> mapNode = new HashMap<>();
Map<String, ConfigNode> mapNode = new LinkedHashMap<>();

configObject.forEach((key, value) -> {
String newPath = normalizeSentence(key);
Expand Down
Loading