diff --git a/README.md b/README.md
index 8b2a4af8..6f231b4a 100644
--- a/README.md
+++ b/README.md
@@ -160,7 +160,32 @@ You can configure the version and properties adjustments for specific branches a
e.g `${env.BUILD_NUMBER:-0}` or `${env.BUILD_NUMBER:-local}`
ℹ define placeholder overwrite value (placeholder is defined) like this `${name:+OVERWRITE_VALUE}`
-e.g `${dirty:-SNAPSHOT}` resolves to `-SNAPSHOT` instead of `-DIRTY`
+e.g `${dirty:+-SNAPSHOT}` resolves to `-SNAPSHOT` instead of `-DIRTY`
+
+ℹ placeholder default values and overwrite values containing the `:` character can escape it by doubling it
+in case the value matches with a function name. For example, `${env.VAR:-hello::slug}` resolves to `hello:slug`
+if `env.VAR` is not defined.
+
+###### Placeholders functions
+Placeholders can contain functions with the form: `${key:functionname}` where the function `functionname` is applied to
+the value referenced by `key`.
+
+They can be combined with format placeholders and combined, e.g. `${key:-abc/def/42:slug:uppercase:next}` will give
+`ABD-DEF-43` if key is absent.
+
+Defined functions:
+- `slug`: replaces all sequences of characters that are not alphanumeric or underscore or hyphen with and hyphen and
+ eliminate duplicated hyphens from the value.
+- `slug+dot`: does the same thing as `slug` but does not replace `.` characters.
+- `slug+hyphen`: does the same thing as `slug` but replaces also `_` with `-`.
+- `slug+hyphen+dot`: does the same thing as `slug+hyphen` but does not replace `.` characters.
+- `word`: does the same thing as `slug` but replaces `-` and all non-alphanumeric characters with `_`.
+- `word+dot`: does the same thing as `word` but does not replace `.` characters.
+- `uppercase`: transform the value to uppercase.
+- `lowercase`: transform the value to lowercase.
+- `next`: if the value ends with a number, it is incremented, else it appends `.1` at the end of the value.
+- `incrementlast`: if the value contains a number, increments it, else does nothing, e.g. if `x` has value `rc.1-something`
+ `${x:incrementlast}` returns `rc.2-something`.
###### Placeholders
diff --git a/src/main/java/me/qoomon/gitversioning/commons/StringUtil.java b/src/main/java/me/qoomon/gitversioning/commons/StringUtil.java
index fda329d3..8ebb3ddb 100644
--- a/src/main/java/me/qoomon/gitversioning/commons/StringUtil.java
+++ b/src/main/java/me/qoomon/gitversioning/commons/StringUtil.java
@@ -2,29 +2,62 @@
import java.util.HashMap;
import java.util.HashSet;
+import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.function.Supplier;
+import java.util.function.UnaryOperator;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
+import java.util.stream.Collectors;
public final class StringUtil {
+ private static final Pattern END_NUMBERS = Pattern.compile("\\d+$");
+ private static final Pattern LAST_NUMBERS = Pattern.compile("(\\d+)(?=\\D*$)");
+ private static final Pattern PLACEHOLDER_PATTERN;
+ private static final Map> FUNCTIONS;
+
+ static {
+ final Map> functions = new HashMap<>();
+ functions.put("slug", str -> str.replaceAll("[^\\w-]+", "-").replaceAll("-{2,}", "-"));
+ functions.put("slug+dot", str -> str.replaceAll("[^\\w.-]+", "-").replaceAll("-{2,}", "-"));
+ functions.put("slug+hyphen", str -> str.replaceAll("[^a-zA-Z0-9-]+", "-").replaceAll("-{2,}", "-"));
+ functions.put("slug+hyphen+dot", str -> str.replaceAll("[^a-zA-Z0-9.-]+", "-").replaceAll("-{2,}", "-"));
+ functions.put("next", StringUtil::next);
+ functions.put("incrementlast", StringUtil::incrementLast);
+ functions.put("uppercase", str -> str.toUpperCase(Locale.ROOT));
+ functions.put("lowercase", str -> str.toLowerCase(Locale.ROOT));
+ functions.put("word", str -> str.replaceAll("\\W+", "_").replaceAll("_{2,}", "_"));
+ functions.put("word+dot", str -> str.replaceAll("[^\\w.]+", "_").replaceAll("_{2,}", "_"));
+ FUNCTIONS = functions.entrySet().stream().collect(Collectors.toUnmodifiableMap(
+ Map.Entry::getKey,
+ e -> str -> str == null ? null : e.getValue().apply(str))
+ );
+ final String functionsAlternatives = FUNCTIONS.keySet().stream().map(Pattern::quote).collect(Collectors.joining("|:", "(?(?::", ")+)?"));
+ PLACEHOLDER_PATTERN = Pattern.compile("\\$\\{(?[^}:]+)(?::(?[-+])(?(?:::|[^:}])*))?" + functionsAlternatives + "}");
+ }
+
public static String substituteText(String text, Map> replacements) {
StringBuffer result = new StringBuffer();
- Pattern placeholderPattern = Pattern.compile("\\$\\{(?[^}:]+)(?::(?[-+])(?[^}]*))?}");
- Matcher placeholderMatcher = placeholderPattern.matcher(text);
+ Matcher placeholderMatcher = PLACEHOLDER_PATTERN.matcher(text);
while (placeholderMatcher.find()) {
String placeholderKey = placeholderMatcher.group("key");
Supplier replacementSupplier = replacements.get(placeholderKey);
String replacement = replacementSupplier != null ? replacementSupplier.get() : null;
String placeholderModifier = placeholderMatcher.group("modifier");
- if(placeholderModifier != null){
+ if (placeholderModifier != null) {
if (placeholderModifier.equals("-") && replacement == null) {
- replacement = placeholderMatcher.group("value");
+ replacement = placeholderMatcher.group("value").replace("::", ":");
}
if (placeholderModifier.equals("+") && replacement != null) {
- replacement = placeholderMatcher.group("value");
+ replacement = placeholderMatcher.group("value").replace("::", ":");
+ }
+ }
+ String functionNames = placeholderMatcher.group("functions");
+ if (functionNames != null) {
+ for (String functionName : functionNames.substring(1).split(":")) {
+ replacement = FUNCTIONS.get(functionName).apply(replacement);
}
}
if (replacement != null) {
@@ -39,7 +72,7 @@ public static String substituteText(String text, Map> r
/**
* @param pattern pattern
- * @param text to parse
+ * @param text to parse
* @return a map of group-index and group-name to matching value
*/
public static Map patternGroupValues(Pattern pattern, String text) {
@@ -91,4 +124,22 @@ public static Set patternGroupNames(Pattern pattern) {
return groups;
}
+ public static String next(String value) {
+ final Matcher matcher = END_NUMBERS.matcher(value);
+ if (matcher.find()) {
+ return matcher.replaceAll(matchResult -> String.valueOf(Long.parseLong(matchResult.group()) + 1L));
+ }
+ return value + ".1";
+ }
+
+ public static String incrementLast(String value) {
+ final Matcher matcher = LAST_NUMBERS.matcher(value);
+ if (matcher.find()) {
+ return matcher.replaceFirst(matchResult -> String.valueOf(Long.parseLong(matchResult.group()) + 1L));
+ }
+ return value;
+ }
+
+ private StringUtil() {
+ }
}
diff --git a/src/test/java/me/qoomon/gitversioning/commons/StringUtilTest.java b/src/test/java/me/qoomon/gitversioning/commons/StringUtilTest.java
index 1358d5f4..03a4e921 100644
--- a/src/test/java/me/qoomon/gitversioning/commons/StringUtilTest.java
+++ b/src/test/java/me/qoomon/gitversioning/commons/StringUtilTest.java
@@ -85,6 +85,216 @@ void substituteText_overwrite_value() {
assertThat(outputText).isEqualTo("xxx");
}
+ @Test
+ void substituteText_function_combination_slug_uppercase() {
+
+ // Given
+ String givenText = "${foo:+a/b:slug:uppercase}";
+ Map> givenSubstitutionMap = new HashMap<>();
+ givenSubstitutionMap.put("foo", () -> "aaa");
+
+ // When
+ String outputText = StringUtil.substituteText(givenText, givenSubstitutionMap);
+
+ // Then
+ assertThat(outputText).isEqualTo("A-B");
+ }
+
+ @Test
+ void substituteText_function_word_lowercase() {
+
+ // Given
+ String givenText = "${foo:word:lowercase}";
+ Map> givenSubstitutionMap = new HashMap<>();
+ givenSubstitutionMap.put("foo", () -> "PR-56+/ii");
+
+ // When
+ String outputText = StringUtil.substituteText(givenText, givenSubstitutionMap);
+
+ // Then
+ assertThat(outputText).isEqualTo("pr_56_ii");
+ }
+
+ @Test
+ void substituteText_function_slug() {
+
+ // Given
+ String givenText = "${foo:slug}";
+ Map> givenSubstitutionMap = new HashMap<>();
+ givenSubstitutionMap.put("foo", () -> "PR-56+/ii_7");
+
+ // When
+ String outputText = StringUtil.substituteText(givenText, givenSubstitutionMap);
+
+ // Then
+ assertThat(outputText).isEqualTo("PR-56-ii_7");
+ }
+
+ @Test
+ void substituteText_function_slug_dot() {
+
+ // Given
+ String givenText = "${foo:slug+dot}";
+ Map> givenSubstitutionMap = new HashMap<>();
+ givenSubstitutionMap.put("foo", () -> "my-release/2.5");
+
+ // When
+ String outputText = StringUtil.substituteText(givenText, givenSubstitutionMap);
+
+ // Then
+ assertThat(outputText).isEqualTo("my-release-2.5");
+ }
+
+ @Test
+ void substituteText_function_slug_hyphen() {
+
+ // Given
+ String givenText = "${foo:slug+hyphen}";
+ Map> givenSubstitutionMap = new HashMap<>();
+ givenSubstitutionMap.put("foo", () -> "my_release/2.5");
+
+ // When
+ String outputText = StringUtil.substituteText(givenText, givenSubstitutionMap);
+
+ // Then
+ assertThat(outputText).isEqualTo("my-release-2-5");
+ }
+
+ @Test
+ void substituteText_function_slug_hyphen_dot() {
+
+ // Given
+ String givenText = "${foo:slug+hyphen+dot}";
+ Map> givenSubstitutionMap = new HashMap<>();
+ givenSubstitutionMap.put("foo", () -> "my_release/2.5");
+
+ // When
+ String outputText = StringUtil.substituteText(givenText, givenSubstitutionMap);
+
+ // Then
+ assertThat(outputText).isEqualTo("my-release-2.5");
+ }
+
+ @Test
+ void substituteText_function_word_dot() {
+
+ // Given
+ String givenText = "${foo:word+dot}";
+ Map> givenSubstitutionMap = new HashMap<>();
+ givenSubstitutionMap.put("foo", () -> "release/2.5");
+
+ // When
+ String outputText = StringUtil.substituteText(givenText, givenSubstitutionMap);
+
+ // Then
+ assertThat(outputText).isEqualTo("release_2.5");
+ }
+
+ @Test
+ void substituteText_function_next() {
+
+ // Given
+ String givenText = "${foo:next}";
+ Map> givenSubstitutionMap = new HashMap<>();
+ givenSubstitutionMap.put("foo", () -> "alpha.56-rc.12");
+
+ // When
+ String outputText = StringUtil.substituteText(givenText, givenSubstitutionMap);
+
+ // Then
+ assertThat(outputText).isEqualTo("alpha.56-rc.13");
+ }
+
+ @Test
+ void substituteText_function_next_without_number_adds_dot_1() {
+
+ // Given
+ String givenText = "${foo:next}";
+ Map> givenSubstitutionMap = new HashMap<>();
+ givenSubstitutionMap.put("foo", () -> "alpha.56-rc.12-abc");
+
+ // When
+ String outputText = StringUtil.substituteText(givenText, givenSubstitutionMap);
+
+ // Then
+ assertThat(outputText).isEqualTo("alpha.56-rc.12-abc.1");
+ }
+
+ @Test
+ void substituteText_function_incrementlast() {
+
+ // Given
+ String givenText = "${foo:incrementlast}";
+ Map> givenSubstitutionMap = new HashMap<>();
+ givenSubstitutionMap.put("foo", () -> "alpha.56-rc.12");
+
+ // When
+ String outputText = StringUtil.substituteText(givenText, givenSubstitutionMap);
+
+ // Then
+ assertThat(outputText).isEqualTo("alpha.56-rc.13");
+ }
+
+ @Test
+ void substituteText_function_incrementlast_without_number_does_nothing() {
+
+ // Given
+ String givenText = "${foo:incrementlast}";
+ Map> givenSubstitutionMap = new HashMap<>();
+ givenSubstitutionMap.put("foo", () -> "alpha");
+
+ // When
+ String outputText = StringUtil.substituteText(givenText, givenSubstitutionMap);
+
+ // Then
+ assertThat(outputText).isEqualTo("alpha");
+ }
+
+ @Test
+ void substituteText_function_incrementlast_without_number_at_end() {
+
+ // Given
+ String givenText = "${foo:incrementlast}";
+ Map> givenSubstitutionMap = new HashMap<>();
+ givenSubstitutionMap.put("foo", () -> "alpha.9-special");
+
+ // When
+ String outputText = StringUtil.substituteText(givenText, givenSubstitutionMap);
+
+ // Then
+ assertThat(outputText).isEqualTo("alpha.10-special");
+ }
+
+ @Test
+ void substituteText_function_value_escaping() {
+
+ // Given
+ String givenText = "${foo:+word::lowercase}";
+ Map> givenSubstitutionMap = new HashMap<>();
+ givenSubstitutionMap.put("foo", () -> "PR-56+/ii");
+
+ // When
+ String outputText = StringUtil.substituteText(givenText, givenSubstitutionMap);
+
+ // Then
+ assertThat(outputText).isEqualTo("word:lowercase");
+ }
+
+ @Test
+ void substituteText_function_value_escaping_with_function() {
+
+ // Given
+ String givenText = "${foo:+WORD:::lowercase}";
+ Map> givenSubstitutionMap = new HashMap<>();
+ givenSubstitutionMap.put("foo", () -> "word");
+
+ // When
+ String outputText = StringUtil.substituteText(givenText, givenSubstitutionMap);
+
+ // Then
+ assertThat(outputText).isEqualTo("word:");
+ }
+
@Test
void valueGroupMap() {