Skip to content

Commit

Permalink
Merge pull request #84 from gestalt-config/feat/substitutionDefaults
Browse files Browse the repository at this point in the history
You can provide a default for the substitution
  • Loading branch information
credmond-git authored Apr 26, 2023
2 parents 3890ede + acc0c40 commit 7fd89ec
Show file tree
Hide file tree
Showing 10 changed files with 171 additions and 17 deletions.
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,7 +370,14 @@ You can specify the substitution in the format ${transform:key} or ${key}. If yo
Unlike the rest of Gestalt, this is case-sensitive, and it does not tokenize the string (except the node transform). The key expects an exact match, so if the Environment Variable name is DB_USER you need to use the key DB_USER, db.user or db_user will not match.

```properties
db.uri=jdbc:mysql://${env:DB_HOST}:${map:DB_PORT}/${sys:environment}
db.uri=jdbc:mysql://${DB_HOST}:${map:DB_PORT}/${sys:environment}
```

### Defaults for a Substitution
You can provide a default for the substitution in the format ${transform:key:=default} or ${key:=default}. If you provide a default it will use the default value in the event that the key provided cant be found

```properties
db.uri=jdbc:mysql://${DB_HOST}:${map:DB_PORT:=3306}/${environment:=dev}
```

### Escaping a Substitution
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ plugins {

allprojects {
group = "com.github.gestalt-config"
version = "0.20.4"
version = "0.20.5"
}


Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ public class GestaltBuilder {
// the maximum nested substitution depth.
private Integer maxSubstitutionNestedDepth = null;

// the regex used to parse string substitutions.
// Must have a named capture group transform, key, and default, where the key is required and the transform and default are optional.
private String substitutionRegex = null;

/**
* Adds all default decoders to the builder. Uses the ServiceLoader to find all registered Decoders and adds them
*
Expand Down Expand Up @@ -613,6 +617,26 @@ public void setMaxSubstitutionNestedDepth(Integer maxSubstitutionNestedDepth) {
this.maxSubstitutionNestedDepth = maxSubstitutionNestedDepth;
}

/**
* the regex used to parse string substitutions.
* Must have a named capture group transform, key, and default, where the key is required and the transform and default are optional.
*
* @return the string substitution regex
*/
public String getSubstitutionRegex() {
return substitutionRegex;
}

/**
* the regex used to parse string substitutions.
* Must have a named capture group transform, key, and default, where the key is required and the transform and default are optional.
*
* @param substitutionRegex the string substitution regex
*/
public void setSubstitutionRegex(String substitutionRegex) {
this.substitutionRegex = substitutionRegex;
}


/**
* dedupe decoders and return the deduped list.
Expand Down Expand Up @@ -767,6 +791,9 @@ private GestaltConfig rebuildConfig() {
newConfig.setMaxSubstitutionNestedDepth(Objects.requireNonNullElseGet(maxSubstitutionNestedDepth,
() -> gestaltConfig.getMaxSubstitutionNestedDepth()));

newConfig.setSubstitutionRegex(Objects.requireNonNullElseGet(substitutionRegex,
() -> gestaltConfig.getSubstitutionRegex()));

return newConfig;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.github.gestalt.config.entity;

import org.github.gestalt.config.post.process.transform.TransformerPostProcessor;

import java.time.format.DateTimeFormatter;

/**
Expand Down Expand Up @@ -35,6 +37,10 @@ public class GestaltConfig {
// the maximum nested substitution depth.
private int maxSubstitutionNestedDepth = 5;

// the regex used to parse string substitutions.
// Must have a named capture group transform, key, and default, where the key is required and the transform and default are optional.
private String substitutionRegex = TransformerPostProcessor.defaultSubstitutionRegex;

/**
* Treat all warnings as errors.
*
Expand Down Expand Up @@ -216,4 +222,25 @@ public int getMaxSubstitutionNestedDepth() {
public void setMaxSubstitutionNestedDepth(int maxSubstitutionNestedDepth) {
this.maxSubstitutionNestedDepth = maxSubstitutionNestedDepth;
}


/**
* the regex used to parse string substitutions.
* Must have a named capture group transform, key, and default, where the key is required and the transform and default are optional.
*
* @return the string substitution regex
*/
public String getSubstitutionRegex() {
return substitutionRegex;
}

/**
* the regex used to parse string substitutions.
* Must have a named capture group transform, key, and default, where the key is required and the transform and default are optional.
*
* @param substitutionRegex the string substitution regex
*/
public void setSubstitutionRegex(String substitutionRegex) {
this.substitutionRegex = substitutionRegex;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1115,6 +1115,25 @@ public String description() {
}
}

/**
* Transform doesnt match the regex
*/
public static class TransformDoesntMatchRegex extends ValidationError {
private final String path;
private final String value;

public TransformDoesntMatchRegex(String path, String value) {
super(ValidationLevel.ERROR);
this.path = path;
this.value = value;
}

@Override
public String description() {
return "Transform doesnt match the expected format with value " + value + " on path " + path;
}
}

/**
* Not a valid SubstitutionNode
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,10 @@
* @author <a href="mailto:colin.redmond@outlook.com"> Colin Redmond </a> (c) 2023.
*/
public class TransformerPostProcessor implements PostProcessor {
private static final Pattern pattern = Pattern.compile(
"((?<transform>\\w+):)?(?<key>[\\w ,_.+;:\"'`~!@#$%^&*()\\[\\]<>]+)"
);

public static final String defaultSubstitutionRegex =
"^((?<transform>\\w+):)?(?<key>[\\w ,_.+;\"'`~!@#$%^&*()\\[\\]<>]+)(:=(?<default>[\\w ,_.+;:\"'`~!@#$%^&*()\\[\\]<>]+))?$";
private Pattern pattern;

private final Map<String, Transformer> transformers;
private final List<Transformer> orderedDefaultTransformers;
Expand All @@ -55,6 +56,7 @@ public TransformerPostProcessor() {
});

this.orderedDefaultTransformers = buildOrderedConfigPriorities(transformersList, false);
this.pattern = Pattern.compile(defaultSubstitutionRegex);
}

/**
Expand All @@ -70,7 +72,9 @@ public TransformerPostProcessor(List<Transformer> transformers) {
this.transformers = transformers.stream().collect(Collectors.toMap(Transformer::name, Function.identity()));
this.orderedDefaultTransformers = buildOrderedConfigPriorities(transformers, false);
}

this.substitutionTreeBuilder = new SubstitutionTreeBuilder("${", "}");
this.pattern = Pattern.compile(defaultSubstitutionRegex);
}

@Override
Expand All @@ -81,6 +85,7 @@ public void applyConfig(PostProcessorConfig config) {
config.getConfig().getSubstitutionClosingToken());

this.maxRecursionDepth = config.getConfig().getMaxSubstitutionNestedDepth();
this.pattern = Pattern.compile(defaultSubstitutionRegex);
}

@Override
Expand Down Expand Up @@ -149,9 +154,11 @@ private ValidateOf<String> transformString(String path, String input) {
Matcher matcher = pattern.matcher(input);
StringBuilder newLeafValue = new StringBuilder();
boolean foundMatch = false;

while (matcher.find()) {
String transformName = matcher.group("transform");
String key = matcher.group("key");
String defaultValue = matcher.group("default");

// if we have a named transform look it up in the map.
if (transformName != null) {
Expand All @@ -161,7 +168,13 @@ private ValidateOf<String> transformString(String path, String input) {
newLeafValue.append(transformValue.results());
foundMatch = true;
} else {
return ValidateOf.inValid(new ValidationError.NoKeyFoundForTransform(path, transformName, key));
// if we have no results from the transform but a default value, use the default
if (defaultValue != null) {
foundMatch = true;
newLeafValue.append(defaultValue);
} else {
return ValidateOf.inValid(new ValidationError.NoKeyFoundForTransform(path, transformName, key));
}
}
} else {
return ValidateOf.inValid(new ValidationError.NoMatchingTransformFound(path, transformName));
Expand All @@ -180,15 +193,21 @@ private ValidateOf<String> transformString(String path, String input) {
}

if (!foundTransformer) {
return ValidateOf.inValid(new ValidationError.NoMatchingDefaultTransformFound(path, key));
// if we have no results from the transform but a default value, use the default
if (defaultValue != null) {
foundMatch = true;
newLeafValue.append(defaultValue);
} else {
return ValidateOf.inValid(new ValidationError.NoMatchingDefaultTransformFound(path, key));
}
}
}
}

if (foundMatch) {
return ValidateOf.valid(newLeafValue.toString());
} else {
return ValidateOf.valid(input);
return ValidateOf.inValid(new ValidationError.TransformDoesntMatchRegex(path, input));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,61 @@ void processTextWithMultipleTransform() {
Assertions.assertEquals("hello world it is sunny today", validateNode.results().getValue().get());
}

@Test
void processTextWithMultipleDefaults() {

Map<String, String> customMap = new HashMap<>();
CustomMapTransformer transformer = new CustomMapTransformer(customMap);

TransformerPostProcessor transformerPostProcessor = new TransformerPostProcessor(Collections.singletonList(transformer));
LeafNode node = new LeafNode("hello ${map:place:=world} it is ${weather:=sunny} today");
ValidateOf<ConfigNode> validateNode = transformerPostProcessor.process("location", node);

Assertions.assertFalse(validateNode.hasErrors());
Assertions.assertTrue(validateNode.hasResults());
Assertions.assertTrue(validateNode.results().getValue().isPresent());
Assertions.assertEquals("hello world it is sunny today", validateNode.results().getValue().get());
}

@Test
void processTextWithMultipleDefaultsButHasValues() {

Map<String, String> customMap = new HashMap<>();
customMap.put("place", "world");
customMap.put("weather", "sunny");
CustomMapTransformer transformer = new CustomMapTransformer(customMap);

TransformerPostProcessor transformerPostProcessor = new TransformerPostProcessor(Collections.singletonList(transformer));
LeafNode node = new LeafNode("hello ${map:place:=earth} it is ${weather:=overcast} today");
ValidateOf<ConfigNode> validateNode = transformerPostProcessor.process("location", node);

Assertions.assertFalse(validateNode.hasErrors());
Assertions.assertTrue(validateNode.hasResults());
Assertions.assertTrue(validateNode.results().getValue().isPresent());
Assertions.assertEquals("hello world it is sunny today", validateNode.results().getValue().get());
}

@Test
void processInvalidFormat() {
// not sure about this test, this isnt "intended" behaviour. It just happens to happen.

Map<String, String> customMap = new HashMap<>();
customMap.put("place", "world");
customMap.put("weather", "sunny");
CustomMapTransformer transformer = new CustomMapTransformer(customMap);

TransformerPostProcessor transformerPostProcessor = new TransformerPostProcessor(Collections.singletonList(transformer));
LeafNode node = new LeafNode("${map:place:world}");
ValidateOf<ConfigNode> validateNode = transformerPostProcessor.process("location", node);

Assertions.assertTrue(validateNode.hasErrors());
Assertions.assertEquals(1, validateNode.getErrors().size());
Assertions.assertEquals("Transform doesnt match the expected format with value map:place:world on path location",
validateNode.getErrors().get(0).description());
Assertions.assertEquals(ValidationLevel.ERROR, validateNode.getErrors().get(0).level());

}

@Test
void processNoValue() {
Map<String, String> customMap = new HashMap<>();
Expand Down
4 changes: 2 additions & 2 deletions gestalt-core/src/test/resources/defaultPPEnv.properties
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ 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.idleTimeout=${envVar:DB_IDLETIMEOUT}
db.maxLifetime=60000.0
db.idleTimeout=${envVar:DB_IDLETIMEOUT:=900}
db.maxLifetime=${envVar:NO_RESULTS_FOUND:=60000.0}
http.Pool.maxTotal=100
http.Pool.maxPerRoute=10
http.Pool.validateAfterInactivity=6000
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ 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.idleTimeout=${envVar:DB_IDLETIMEOUT}
db.maxLifetime=60000.0
db.idleTimeout=${envVar:DB_IDLETIMEOUT:=900}
db.maxLifetime=${envVar:NO_RESULTS_FOUND:=60000.0}
http.Pool.maxTotal=100
http.Pool.maxPerRoute=10
http.Pool.validateAfterInactivity=6000
Expand Down
10 changes: 5 additions & 5 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,20 +1,20 @@
[versions]
java = "11"
javaLatest = "19"
kotlin = "1.8.20"
kotlin = "1.8.21"
kotlinDokka = "1.8.10"
kodeinDI = "7.20.1"
koinDI= "3.4.0"

jackson = "2.14.2"
jackson = "2.15.0"

guice = "5.1.0"
cdi = "3.0.0"
weld = "3.1.0.Final"
weldCore = "4.0.3.Final"

hocon = "1.4.2"
aws = "2.20.48"
aws = "2.20.52"
jgit = "6.5.0.202303070854-r"

eddsa = "0.3.0"
Expand All @@ -23,7 +23,7 @@ junit5 = "5.9.2"
assertJ = "3.24.2"
mockito = "5.2.0"
mockk = "1.13.5"
koTestAssertions = "5.5.5"
koTestAssertions = "5.6.1"
awsMock = "2.11.0"

errorprone = "2.18.0"
Expand All @@ -37,7 +37,7 @@ pmd = "6.54.0"
jmh = "1.36"
gradleJmh = "0.7.1"

gestalt = "0.20.3"
gestalt = "0.20.5"

gradleVersions = "0.46.0"
gitVersions = "2.0.0"
Expand Down

0 comments on commit 7fd89ec

Please sign in to comment.