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() {