Skip to content

Commit 20e1f3a

Browse files
Support FormattableString as argument of BicepFunction.Interpolate (#51419)
* initial commit * tweak the implementation and implement some tests for it * a few refactors * export api * resolve comments * export api * add some more xml doc * refine xml docs * add changelog and bump version to 1.3.0 * add a link to issue
1 parent 4153edd commit 20e1f3a

File tree

8 files changed

+524
-13
lines changed

8 files changed

+524
-13
lines changed

sdk/provisioning/Azure.Provisioning/CHANGELOG.md

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,10 @@
11
# Release History
22

3-
## 1.3.0-beta.1 (Unreleased)
3+
## 1.3.0 (2025-08-01)
44

55
### Features Added
66

7-
### Breaking Changes
8-
9-
### Bugs Fixed
10-
11-
### Other Changes
7+
- Supported `FormattableString` in `BicepFunction.Interpolate` method. ([#47360](https://github.com/Azure/azure-sdk-for-net/issues/47360))
128

139
## 1.2.1 (2025-07-09)
1410

sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.net8.0.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -610,6 +610,7 @@ public ref partial struct BicepInterpolatedStringHandler
610610
public BicepInterpolatedStringHandler(int literalLength, int formattedCount) { throw null; }
611611
public void AppendFormatted<T>(T t) { }
612612
public void AppendLiteral(string text) { }
613+
public static implicit operator Azure.Provisioning.Expressions.BicepInterpolatedStringHandler (System.FormattableString formattable) { throw null; }
613614
}
614615
public partial class BicepProgram
615616
{

sdk/provisioning/Azure.Provisioning/api/Azure.Provisioning.netstandard2.0.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -608,6 +608,7 @@ public ref partial struct BicepInterpolatedStringHandler
608608
public BicepInterpolatedStringHandler(int literalLength, int formattedCount) { throw null; }
609609
public void AppendFormatted<T>(T t) { }
610610
public void AppendLiteral(string text) { }
611+
public static implicit operator Azure.Provisioning.Expressions.BicepInterpolatedStringHandler (System.FormattableString formattable) { throw null; }
611612
}
612613
public partial class BicepProgram
613614
{

sdk/provisioning/Azure.Provisioning/src/Azure.Provisioning.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
<PropertyGroup>
44
<Description>Contains the core functionality for defining Azure infrastructure with dotnet code.</Description>
5-
<Version>1.3.0-beta.1</Version>
5+
<Version>1.3.0</Version>
66
<!--The ApiCompatVersion is managed automatically and should not generally be modified manually.-->
77
<ApiCompatVersion>1.2.1</ApiCompatVersion>
88
<TargetFrameworks>$(RequiredTargetFrameworks)</TargetFrameworks>

sdk/provisioning/Azure.Provisioning/src/Expressions/BicepFunction.cs

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
using System.Linq;
66
using Azure.Core;
77
using Azure.Provisioning.Resources;
8+
using Azure.Provisioning.Utilities;
89

910
namespace Azure.Provisioning.Expressions;
1011

@@ -293,8 +294,7 @@ public static BicepValue<string> ToUpper(BicepValue<object> value) =>
293294
/// Combines multiple string values <paramref name="values"/> and returns
294295
/// the concatenated string. This represents the <c>concat</c> Bicep
295296
/// function. To improve readability, prefer
296-
/// <see cref="BicepFunction.Interpolate"/> instead of
297-
/// <see cref="Concat"/>.
297+
/// <see cref="Interpolate"/> instead of <see cref="Concat"/>.
298298
/// </summary>
299299
/// <param name="values">Strings in sequential order for concatenation.</param>
300300
/// <returns>A string or array of concatenated values.</returns>
@@ -310,11 +310,17 @@ public static BicepValue<string> Concat(params BicepValue<string>[] values)
310310
}
311311

312312
/// <summary>
313-
/// Convert a formattable string with literal text, C# expressions, and
314-
/// Bicep expressions into an interpolated Bicep string.
313+
/// Builds a Bicep interpolated string expression from C# interpolated string syntax
314+
/// or a <see cref="FormattableString"/> instance.
315+
/// Use this method to combine literal text, C# expressions and Bicep expressions
316+
/// into a single Bicep string value.
315317
/// </summary>
316-
/// <param name="handler">A bicep interpolated string handler.</param>
317-
/// <returns>An interpolated string.</returns>
318+
/// <param name="handler">
319+
/// The <see cref="BicepInterpolatedStringHandler"/> that collects literal and formatted segments from the interpolated string.
320+
/// </param>
321+
/// <returns>
322+
/// A <see cref="BicepValue{String}"/> representing the constructed Bicep interpolated string expression of type <c>string</c>.
323+
/// </returns>
318324
public static BicepValue<string> Interpolate(BicepInterpolatedStringHandler handler) =>
319325
handler.Build();
320326
}

sdk/provisioning/Azure.Provisioning/src/Expressions/BicepStringBuilder.cs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
// Copyright (c) Microsoft Corporation. All rights reserved.
22
// Licensed under the MIT License.
33

4+
using System;
45
using System.Collections.Generic;
56
using System.Runtime.CompilerServices;
7+
using Azure.Provisioning.Utilities;
68

79
namespace Azure.Provisioning.Expressions;
810

@@ -108,6 +110,10 @@ public void AppendFormatted<T>(T t)
108110
{
109111
_expressions.Add(exp);
110112
}
113+
else if (t is FormattableString formattable)
114+
{
115+
AppendFormattableString(formattable);
116+
}
111117
else
112118
{
113119
string? s = t?.ToString();
@@ -118,10 +124,34 @@ public void AppendFormatted<T>(T t)
118124
}
119125
}
120126

127+
internal void AppendFormattableString(FormattableString value)
128+
{
129+
var formatSpan = value.Format.AsSpan();
130+
foreach (var (span, isLiteral, index) in FormattableStringHelpers.GetFormattableStringFormatParts(formatSpan))
131+
{
132+
if (isLiteral)
133+
{
134+
AppendLiteral(span.ToString());
135+
}
136+
else
137+
{
138+
// this is not a literal therefore an argument
139+
AppendFormatted(value.GetArgument(index));
140+
}
141+
}
142+
}
143+
121144
internal readonly BicepValue<string> Build()
122145
{
123146
BicepValue<string> value = new(new InterpolatedStringExpression([.. _expressions]));
124147
value._isSecure = _isSecure;
125148
return value;
126149
}
150+
151+
public static implicit operator BicepInterpolatedStringHandler(FormattableString formattable)
152+
{
153+
var handler = new BicepInterpolatedStringHandler(formattable.Format.Length, formattable.ArgumentCount);
154+
handler.AppendFormattableString(formattable);
155+
return handler;
156+
}
127157
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
6+
namespace Azure.Provisioning.Utilities
7+
{
8+
internal static class FormattableStringHelpers
9+
{
10+
/// <summary>
11+
/// Parses the format from a <see cref="FormattableString"/> and returns an enumerator
12+
/// that yields the parts of the format.
13+
/// </summary>
14+
/// <param name="format"></param>
15+
/// <returns></returns>
16+
public static GetFormatPartsEnumerator GetFormattableStringFormatParts(ReadOnlySpan<char> format) => new GetFormatPartsEnumerator(format);
17+
18+
public ref struct GetFormatPartsEnumerator
19+
{
20+
private ReadOnlySpan<char> _format;
21+
public Part Current { get; private set; }
22+
23+
public GetFormatPartsEnumerator(ReadOnlySpan<char> format)
24+
{
25+
_format = format;
26+
Current = default;
27+
}
28+
29+
public readonly GetFormatPartsEnumerator GetEnumerator() => this;
30+
31+
public bool MoveNext()
32+
{
33+
if (_format.Length == 0)
34+
{
35+
return false;
36+
}
37+
38+
var separatorIndex = _format.IndexOfAny('{', '}');
39+
40+
if (separatorIndex == -1)
41+
{
42+
Current = new Part(_format, true);
43+
_format = ReadOnlySpan<char>.Empty;
44+
return true;
45+
}
46+
47+
var separator = _format[separatorIndex];
48+
// Handle {{ and }} escape sequences
49+
if (separatorIndex + 1 < _format.Length && _format[separatorIndex + 1] == separator)
50+
{
51+
Current = new Part(_format.Slice(0, separatorIndex + 1), true);
52+
_format = _format.Slice(separatorIndex + 2);
53+
return true;
54+
}
55+
56+
var isLiteral = separator == '{';
57+
58+
// Skip empty literals
59+
if (isLiteral && separatorIndex == 0 && _format.Length > 1)
60+
{
61+
separatorIndex = _format.IndexOf('}');
62+
if (separatorIndex == -1)
63+
{
64+
Current = new Part(_format.Slice(1), true);
65+
_format = ReadOnlySpan<char>.Empty;
66+
return true;
67+
}
68+
69+
Current = new Part(_format.Slice(1, separatorIndex - 1), false);
70+
}
71+
else
72+
{
73+
Current = new Part(_format.Slice(0, separatorIndex), isLiteral);
74+
}
75+
76+
_format = _format.Slice(separatorIndex + 1);
77+
return true;
78+
}
79+
80+
internal readonly ref struct Part
81+
{
82+
public Part(ReadOnlySpan<char> span, bool isLiteral)
83+
{
84+
Span = span;
85+
IsLiteral = isLiteral;
86+
}
87+
88+
public ReadOnlySpan<char> Span { get; }
89+
public bool IsLiteral { get; }
90+
91+
public void Deconstruct(out ReadOnlySpan<char> span, out bool isLiteral, out int argumentIndex)
92+
{
93+
span = Span;
94+
isLiteral = IsLiteral;
95+
96+
if (IsLiteral)
97+
{
98+
argumentIndex = -1;
99+
}
100+
else
101+
{
102+
var formatSeparatorIndex = span.IndexOf(':');
103+
var indexSpan = formatSeparatorIndex == -1 ? span : span.Slice(0, formatSeparatorIndex);
104+
#if NET8_0_OR_GREATER
105+
argumentIndex = int.Parse(indexSpan);
106+
#else
107+
argumentIndex = int.Parse(indexSpan.ToString());
108+
#endif
109+
}
110+
}
111+
}
112+
}
113+
}
114+
}

0 commit comments

Comments
 (0)