@@ -5,78 +5,100 @@ namespace GitVersion.Helpers;
55
66internal static class StringFormatWithExtension
77{
8- internal static IExpressionCompiler ExpressionCompiler { get ; set ; } = new ExpressionCompiler ( ) ;
8+ internal static IExpressionCompiler ExpressionCompiler { get ; set ; } = new ExpressionCompiler ( ) ;
9+
10+ internal static IInputSanitizer InputSanitizer { get ; set ; } = new InputSanitizer ( ) ;
11+
12+ internal static IMemberResolver MemberResolver { get ; set ; } = new MemberResolver ( ) ;
13+
14+ /// <summary>
15+ /// Formats the <paramref name="template"/>, replacing each expression wrapped in curly braces
16+ /// with the corresponding property from the <paramref name="source"/> or <paramref name="environment"/>.
17+ /// </summary>
18+ /// <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>
19+ /// <param name="source">The source object to apply to the <paramref name="template"/></param>
20+ /// <param name="environment"></param>
21+ /// <exception cref="ArgumentNullException">The <paramref name="template"/> is null.</exception>
22+ /// <exception cref="ArgumentException">An environment variable was null and no fallback was provided.</exception>
23+ /// <remarks>
24+ /// An expression containing "." is treated as a property or field access on the <paramref name="source"/>.
25+ /// An expression starting with "env:" is replaced with the value of the corresponding variable from the <paramref name="environment"/>.
26+ /// Each expression may specify a single hardcoded fallback value using the {Prop ?? "fallback"} syntax, which applies if the expression evaluates to null.
27+ /// </remarks>
28+ /// <example>
29+ /// // replace an expression with a property value
30+ /// "Hello {Name}".FormatWith(new { Name = "Fred" }, env);
31+ /// "Hello {Name ?? \"Fred\"}".FormatWith(new { Name = GetNameOrNull() }, env);
32+ /// // replace an expression with an environment variable
33+ /// "{env:BUILD_NUMBER}".FormatWith(new { }, env);
34+ /// "{env:BUILD_NUMBER ?? \"0\"}".FormatWith(new { }, env);
35+ /// </example>
36+ public static string FormatWith < T > ( this string template , T ? source , IEnvironment environment )
37+ {
38+ ArgumentNullException . ThrowIfNull ( template ) ;
39+ ArgumentNullException . ThrowIfNull ( source ) ;
40+
41+ var result = new StringBuilder ( ) ;
42+ var lastIndex = 0 ;
43+
44+ foreach ( var match in RegexPatterns . Common . ExpandTokensRegex ( ) . Matches ( template ) . Cast < Match > ( ) )
45+ {
46+ var replacement = EvaluateMatch ( match , source , environment ) ;
47+ result . Append ( template , lastIndex , match . Index - lastIndex ) ;
48+ result . Append ( replacement ) ;
49+ lastIndex = match . Index + match . Length ;
50+ }
951
10- internal static IInputSanitizer InputSanitizer { get ; set ; } = new InputSanitizer ( ) ;
52+ result . Append ( template , lastIndex , template . Length - lastIndex ) ;
53+ return result . ToString ( ) ;
54+ }
1155
12- internal static IMemberResolver MemberResolver { get ; set ; } = new MemberResolver ( ) ;
56+ private static string EvaluateMatch < T > ( Match match , T source , IEnvironment environment )
57+ {
58+ var fallback = match . Groups [ "fallback" ] . Success ? match . Groups [ "fallback" ] . Value : null ;
1359
14- public static string FormatWith < T > ( this string template , T ? source , IEnvironment environment )
60+ if ( match . Groups [ "envvar" ] . Success )
1561 {
16- ArgumentNullException . ThrowIfNull ( template ) ;
17- ArgumentNullException . ThrowIfNull ( source ) ;
18-
19- var result = new StringBuilder ( ) ;
20- var lastIndex = 0 ;
21-
22- foreach ( var match in RegexPatterns . Common . ExpandTokensRegex ( ) . Matches ( template ) . Cast < Match > ( ) )
23- {
24- var replacement = EvaluateMatch ( match , source , environment ) ;
25- result . Append ( template , lastIndex , match . Index - lastIndex ) ;
26- result . Append ( replacement ) ;
27- lastIndex = match . Index + match . Length ;
28- }
29-
30- result . Append ( template , lastIndex , template . Length - lastIndex ) ;
31- return result . ToString ( ) ;
62+ return EvaluateEnvVar ( match . Groups [ "envvar" ] . Value , fallback , environment ) ;
3263 }
3364
34- private static string EvaluateMatch < T > ( Match match , T source , IEnvironment environment )
65+ if ( match . Groups [ "member" ] . Success )
3566 {
36- var fallback = match . Groups [ "fallback" ] . Success ? match . Groups [ "fallback" ] . Value : null ;
37-
38- if ( match . Groups [ "envvar" ] . Success )
39- {
40- return EvaluateEnvVar ( match . Groups [ "envvar" ] . Value , fallback , environment ) ;
41- }
42-
43- if ( match . Groups [ "member" ] . Success )
44- {
45- var format = match . Groups [ "format" ] . Success ? match . Groups [ "format" ] . Value : null ;
46- return EvaluateMember ( source , match . Groups [ "member" ] . Value , format , fallback ) ;
47- }
48-
49- throw new ArgumentException ( $ "Invalid token format: '{ match . Value } '") ;
67+ var format = match . Groups [ "format" ] . Success ? match . Groups [ "format" ] . Value : null ;
68+ return EvaluateMember ( source , match . Groups [ "member" ] . Value , format , fallback ) ;
5069 }
5170
52- private static string EvaluateEnvVar ( string name , string ? fallback , IEnvironment env )
71+ throw new ArgumentException ( $ "Invalid token format: '{ match . Value } '") ;
72+ }
73+
74+ private static string EvaluateEnvVar ( string name , string ? fallback , IEnvironment env )
75+ {
76+ var safeName = InputSanitizer . SanitizeEnvVarName ( name ) ;
77+ return env . GetEnvironmentVariable ( safeName )
78+ ?? fallback
79+ ?? throw new ArgumentException ( $ "Environment variable { safeName } not found and no fallback provided") ;
80+ }
81+
82+ private static string EvaluateMember < T > ( T source , string member , string ? format , string ? fallback )
83+ {
84+ var safeMember = InputSanitizer . SanitizeMemberName ( member ) ;
85+ var memberPath = MemberResolver . ResolveMemberPath ( source ! . GetType ( ) , safeMember ) ;
86+ var getter = ExpressionCompiler . CompileGetter ( source . GetType ( ) , memberPath ) ;
87+ var value = getter ( source ) ;
88+
89+ if ( value is null )
5390 {
54- var safeName = InputSanitizer . SanitizeEnvVarName ( name ) ;
55- return env . GetEnvironmentVariable ( safeName )
56- ?? fallback
57- ?? throw new ArgumentException ( $ "Environment variable { safeName } not found and no fallback provided") ;
91+ return fallback ?? string . Empty ;
5892 }
5993
60- private static string EvaluateMember < T > ( T source , string member , string ? format , string ? fallback )
94+ if ( format is not null && ValueFormatter . TryFormat (
95+ value ,
96+ InputSanitizer . SanitizeFormat ( format ) ,
97+ out var formatted ) )
6198 {
62- var safeMember = InputSanitizer . SanitizeMemberName ( member ) ;
63- var memberPath = MemberResolver . ResolveMemberPath ( source ! . GetType ( ) , safeMember ) ;
64- var getter = ExpressionCompiler . CompileGetter ( source . GetType ( ) , memberPath ) ;
65- var value = getter ( source ) ;
66-
67- if ( value is null )
68- {
69- return fallback ?? string . Empty ;
70- }
71-
72- if ( format is not null && ValueFormatter . TryFormat (
73- value ,
74- InputSanitizer . SanitizeFormat ( format ) ,
75- out var formatted ) )
76- {
77- return formatted ;
78- }
79-
80- return value . ToString ( ) ?? fallback ?? string . Empty ;
99+ return formatted ;
81100 }
101+
102+ return value . ToString ( ) ?? fallback ?? string . Empty ;
103+ }
82104}
0 commit comments