Skip to content

Commit

Permalink
Feat: add an encoder that will create objects with a constructor with…
Browse files Browse the repository at this point in the history
… a single string parameter. #122
  • Loading branch information
credmond-git committed Dec 4, 2023
1 parent b76d114 commit fd0f650
Show file tree
Hide file tree
Showing 9 changed files with 274 additions and 4 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -773,6 +773,7 @@ To register your own default ConfigLoaders add them to the builder, or add it to
| 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 | |

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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ public <T> ValidateOf<T> decodeNode(String path, Tags tags, ConfigNode configNod
List<Decoder> classDecoder = getDecoderForClass(path, tags, configNode, klass);
classDecoder.sort(Comparator.comparingInt(v -> v.priority().ordinal()));
if (classDecoder.isEmpty()) {
return ValidateOf.inValid(new ValidationError.NoDecodersFound(klass.getName()));
return ValidateOf.inValid(new ValidationError.NoDecodersFound(klass.getName(), configNode));
} else if (classDecoder.size() > 1) {
logger.log(System.Logger.Level.TRACE, "Found multiple decoders for {0}, found: {1}, using {2}: ",
klass, classDecoder, classDecoder.get(0));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
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.node.LeafNode;
import org.github.gestalt.config.reflect.TypeCapture;
import org.github.gestalt.config.tag.Tags;
import org.github.gestalt.config.utils.ValidateOf;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.Arrays;

/**
* Decode a String.
*
* @author <a href="mailto:colin.redmond@outlook.com"> Colin Redmond </a> (c) 2023.
*/
public final class StringConstructorDecoder implements Decoder<Object> {

public StringConstructorDecoder() {
}

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

@Override
public String name() {
return "StringConstructor";
}

@Override
public boolean canDecode(String path, Tags tags, ConfigNode node, TypeCapture<?> type) {
Class<?> klass = type.getRawType();
Constructor<?>[] stringConstructor = klass.getConstructors();

if (!(node instanceof LeafNode)) {
return false;
}

return Arrays.stream(stringConstructor)
.anyMatch(it -> it.getParameterCount() == 1 && it.getParameters()[0].getType().equals(String.class));
}

/**
* Decode the current node. If the current node is a class or list we may need to decode sub nodes.
*
* @param path the current path
* @param tags the tags for the current request
* @param node the current node we are decoding.
* @param type the type of object we are decoding.
* @param decoderContext The context of the current decoder.
* @return ValidateOf the current node with details of either success or failures.
*/
public ValidateOf<Object> decode(String path, Tags tags, ConfigNode node, TypeCapture<?> type, DecoderContext decoderContext) {

LeafNode leafNode = (LeafNode) node;
if (leafNode.getValue().isEmpty()) {
return ValidateOf.inValid(new ValidationError.LeafNodesIsNullDecoding(path, type));
}

Class<?> klass = type.getRawType();

try {
Constructor<?> stringConstructor = klass.getConstructor(String.class);
return ValidateOf.valid(stringConstructor.newInstance(node.getValue().get()));
} catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) {
return ValidateOf.inValid(new ValidationError.StringConstructorNotFound(path, type));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -669,15 +669,17 @@ public String description() {
*/
public static class NoDecodersFound extends ValidationError {
private final String klass;
private final ConfigNode configNode;

public NoDecodersFound(String klass) {
public NoDecodersFound(String klass, ConfigNode configNode) {
super(ValidationLevel.ERROR);
this.klass = klass;
this.configNode = configNode;
}

@Override
public String description() {
return "No decoders found for class: " + klass;
return "No decoders found for class: " + klass + " and node type: " + configNode.getNodeType().getType();
}
}

Expand Down Expand Up @@ -955,6 +957,25 @@ public String description() {
}
}

/**
* Leaf node is null.
*/
public static class LeafNodesIsNullDecoding extends ValidationError {
private final String path;
private final TypeCapture<?> type;

public LeafNodesIsNullDecoding(String path, TypeCapture<?> type) {
super(ValidationLevel.MISSING_VALUE);
this.path = path;
this.type = type;
}

@Override
public String description() {
return "Leaf nodes is null on path: " + path + " decoding type " + type.getRawType().getSimpleName();
}
}

/**
* Leaf node has no values.
*/
Expand All @@ -972,6 +993,25 @@ public String description() {
}
}

/**
* While decoding an Object the string constructor was not found. Unable to create the object.
*/
public static class StringConstructorNotFound extends ValidationError {
private final String path;
private final TypeCapture<?> type;

public StringConstructorNotFound(String path, TypeCapture<?> type) {
super(ValidationLevel.ERROR);
this.path = path;
this.type = type;
}

@Override
public String description() {
return "String Constructor for: " + type.getRawType().getSimpleName() + " is not found on Path: " + path;
}
}

/**
* While decoding a Object the constructor was not public. Unable to create the object.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,4 +29,5 @@ org.github.gestalt.config.decoder.RecordDecoder
org.github.gestalt.config.decoder.SetDecoder
org.github.gestalt.config.decoder.ShortDecoder
org.github.gestalt.config.decoder.StringDecoder
org.github.gestalt.config.decoder.StringConstructorDecoder
org.github.gestalt.config.decoder.UUIDDecoder
Original file line number Diff line number Diff line change
Expand Up @@ -928,7 +928,7 @@ public void testNoEncoder() throws GestaltException {
} catch (GestaltException e) {
assertThat(e).isInstanceOf(GestaltException.class)
.hasMessage("Failed getting config path: db.port, for class: java.lang.Integer\n" +
" - level: ERROR, message: No decoders found for class: java.lang.Integer");
" - level: ERROR, message: No decoders found for class: java.lang.Integer and node type: leaf");
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package org.github.gestalt.config.decoder;

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.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;

import java.util.Map;

import static org.github.gestalt.config.entity.ValidationLevel.ERROR;
import static org.github.gestalt.config.entity.ValidationLevel.MISSING_VALUE;

class StringConstructorDecoderTest {

@Test
void priority() {
StringConstructorDecoder decoder = new StringConstructorDecoder();

Assertions.assertEquals(Priority.LOW, decoder.priority());
}

@Test
void name() {
StringConstructorDecoder decoder = new StringConstructorDecoder();

Assertions.assertEquals("StringConstructor", decoder.name());
}

@Test
void canDecode() {
StringConstructorDecoder decoder = new StringConstructorDecoder();

Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), TypeCapture.of(MyClass.class)));
Assertions.assertFalse(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture<MyClass>() {
}));
Assertions.assertTrue(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture<MyStringClass>() {
}));
Assertions.assertTrue(decoder.canDecode("", Tags.of(), new LeafNode(""), new TypeCapture<MyStringClass>() {
}));

Assertions.assertFalse(decoder.canDecode("", Tags.of(), new MapNode(Map.of()), new TypeCapture<MyStringClass>() {
}));

}

@Test
void decode() {

StringConstructorDecoder decoder = new StringConstructorDecoder();
var results = decoder.decode("hello", Tags.of(), new LeafNode("test"), TypeCapture.of(MyStringClass.class), null);

Assertions.assertTrue(results.hasResults());
Assertions.assertFalse(results.hasErrors());

MyStringClass decoded = (MyStringClass) results.results();

Assertions.assertEquals("test", decoded.myData);
}

@Test
void decodeEmptyLeaf() {

StringConstructorDecoder decoder = new StringConstructorDecoder();
var results = decoder.decode("hello", Tags.of(), new LeafNode(null), TypeCapture.of(MyStringClass.class), null);

Assertions.assertFalse(results.hasResults());
Assertions.assertTrue(results.hasErrors());

Assertions.assertEquals(1, results.getErrors().size());

Assertions.assertEquals(MISSING_VALUE, results.getErrors().get(0).level());
Assertions.assertEquals("Leaf nodes is null on path: hello decoding type MyStringClass",
results.getErrors().get(0).description());
}

@Test
void decodeEmptyWrongType() {

StringConstructorDecoder decoder = new StringConstructorDecoder();
var results = decoder.decode("hello", Tags.of(), new LeafNode("test"), TypeCapture.of(MyClass.class), null);

Assertions.assertFalse(results.hasResults());
Assertions.assertTrue(results.hasErrors());

Assertions.assertEquals(1, results.getErrors().size());

Assertions.assertEquals(ERROR, results.getErrors().get(0).level());
Assertions.assertEquals("String Constructor for: MyClass is not found on Path: hello",
results.getErrors().get(0).description());
}

private static class MyClass {
Integer myData;

public MyClass(Integer myData) {
this.myData = myData;
}

public MyClass() {

}
}

private static class MyStringClass {
String myData;

public MyStringClass(String myData) {
this.myData = myData;
}

public MyStringClass() {

}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,9 @@ private void validateResults(Gestalt gestalt) throws GestaltException {
Assertions.assertEquals(25, pool.idleTimeoutSec);
Assertions.assertEquals(33.0F, pool.defaultWait);

MaxTotal maxTotal = gestalt.getConfig("http.pool.maxTotal", MaxTotal.class);
Assertions.assertEquals(1000, maxTotal.maxTotal);

long startTime = System.nanoTime();
gestalt.getConfig("db", DataBase.class);
long timeTaken = System.nanoTime() - startTime;
Expand Down Expand Up @@ -1150,4 +1153,20 @@ public void setDbPath(String dbPath) {
this.dbPath = dbPath;
}
}

public static class MaxTotal {

private Integer maxTotal;
public MaxTotal(String maxTotal) {
this.maxTotal = Integer.parseInt(maxTotal);
}

public Integer getMaxTotal() {
return maxTotal;
}

public void setMaxTotal(Integer port) {
this.maxTotal = port;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,9 @@ private void validateResults(Gestalt gestalt) throws GestaltException {
Assertions.assertEquals(25, pool.idleTimeoutSec);
Assertions.assertEquals(33.0F, pool.defaultWait);

MaxTotal maxTotal = gestalt.getConfig("http.pool.maxTotal", MaxTotal.class);
Assertions.assertEquals(1000, maxTotal.maxTotal);

Map<String, Integer> httpPoolMap = gestalt.getConfig("http.pool", new TypeCapture<>() { });

Assertions.assertEquals(50, httpPoolMap.get("maxperroute"));
Expand Down Expand Up @@ -1663,4 +1666,20 @@ public void setDataBase(DataBase dataBase) {
this.dataBase = dataBase;
}
}

public static class MaxTotal {

private Integer maxTotal;
public MaxTotal(String maxTotal) {
this.maxTotal = Integer.parseInt(maxTotal);
}

public Integer getMaxTotal() {
return maxTotal;
}

public void setMaxTotal(Integer port) {
this.maxTotal = port;
}
}
}

0 comments on commit fd0f650

Please sign in to comment.