/src/GitVersion.Core/Helpers/StringFormatWith.cs

https://github.com/ParticularLabs/GitVersion · C# · 77 lines · 42 code · 8 blank · 27 comment · 3 complexity · b6999da73ef18a1fea90b536869bd70f MD5 · raw file

  1. using System.Linq.Expressions;
  2. using System.Text.RegularExpressions;
  3. namespace GitVersion.Helpers;
  4. internal static class StringFormatWithExtension
  5. {
  6. // This regex matches an expression to replace.
  7. // - env:ENV name OR a member name
  8. // - optional fallback value after " ?? "
  9. // - the fallback value should be a quoted string, but simple unquoted text is allowed for back compat
  10. private static readonly Regex TokensRegex = new(@"{((env:(?<envvar>\w+))|(?<member>\w+))(\s+(\?\?)??\s+((?<fallback>\w+)|""(?<fallback>.*)""))??}", RegexOptions.Compiled);
  11. /// <summary>
  12. /// Formats the <paramref name="template"/>, replacing each expression wrapped in curly braces
  13. /// with the corresponding property from the <paramref name="source"/> or <paramref name="environment"/>.
  14. /// </summary>
  15. /// <param name="template" this="true">The source template, which may contain expressions to be replaced, e.g '{Foo.Bar.CurrencySymbol} foo {Foo.Bar.Price}'</param>
  16. /// <param name="source">The source object to apply to the <paramref name="template"/></param>
  17. /// <param name="environment"></param>
  18. /// <exception cref="ArgumentNullException">The <paramref name="template"/> is null.</exception>
  19. /// <exception cref="ArgumentException">An environment variable was null and no fallback was provided.</exception>
  20. /// <remarks>
  21. /// An expression containing "." is treated as a property or field access on the <paramref name="source"/>.
  22. /// An expression starting with "env:" is replaced with the value of the corresponding variable from the <paramref name="environment"/>.
  23. /// Each expression may specify a single hardcoded fallback value using the {Prop ?? "fallback"} syntax, which applies if the expression evaluates to null.
  24. /// </remarks>
  25. /// <example>
  26. /// // replace an expression with a property value
  27. /// "Hello {Name}".FormatWith(new { Name = "Fred" }, env);
  28. /// "Hello {Name ?? \"Fred\"}".FormatWith(new { Name = GetNameOrNull() }, env);
  29. /// // replace an expression with an environment variable
  30. /// "{env:BUILD_NUMBER}".FormatWith(new { }, env);
  31. /// "{env:BUILD_NUMBER ?? \"0\"}".FormatWith(new { }, env);
  32. /// </example>
  33. public static string FormatWith<T>(this string template, T? source, IEnvironment environment)
  34. {
  35. if (template == null)
  36. {
  37. throw new ArgumentNullException(nameof(template));
  38. }
  39. foreach (Match match in TokensRegex.Matches(template))
  40. {
  41. string propertyValue;
  42. string? fallback = match.Groups["fallback"].Success ? match.Groups["fallback"].Value : null;
  43. if (match.Groups["envvar"].Success)
  44. {
  45. string envVar = match.Groups["envvar"].Value;
  46. propertyValue = environment.GetEnvironmentVariable(envVar) ?? fallback
  47. ?? throw new ArgumentException($"Environment variable {envVar} not found and no fallback string provided");
  48. }
  49. else
  50. {
  51. var objType = source?.GetType();
  52. string memberAccessExpression = match.Groups["member"].Value;
  53. var expression = CompileDataBinder(objType, memberAccessExpression);
  54. // It would be better to throw if the expression and fallback produce null, but provide an empty string for back compat.
  55. propertyValue = expression(source)?.ToString() ?? fallback ?? "";
  56. }
  57. template = template.Replace(match.Value, propertyValue);
  58. }
  59. return template;
  60. }
  61. private static Func<object?, object> CompileDataBinder(Type? type, string expr)
  62. {
  63. ParameterExpression param = Expression.Parameter(typeof(object));
  64. Expression body = Expression.Convert(param, type);
  65. body = expr.Split('.').Aggregate(body, Expression.PropertyOrField);
  66. body = Expression.Convert(body, typeof(object)); // Convert result in case the body produces a Nullable value type.
  67. return Expression.Lambda<Func<object?, object>>(body, param).Compile();
  68. }
  69. }