Skip to content
5 changes: 3 additions & 2 deletions src/InterfaceStubGenerator.Shared/Emitter.Inline.Method.cs
Original file line number Diff line number Diff line change
Expand Up @@ -149,8 +149,9 @@ internal static void AppendInlineStandardRefitMethod(
.Append(bodyIndent).Append("var ").Append(settingsLocal).Append(" = ").Append(settingsFieldName).AppendLine(";")
.Append(prologue)
.Append(bodyIndent).Append("var ").Append(requestLocal)
.Append(" = new global::System.Net.Http.HttpRequestMessage(").Append(httpMethodExpression)
.Append(", ").Append(requestUriExpression).AppendLine(");")
.AppendLine(" = new global::System.Net.Http.HttpRequestMessage(")
.Append(bodyIndent).Append(httpMethodExpression).AppendLine(",")
.Append(bodyIndent).Append(requestUriExpression).AppendLine(");")
.Append(contentSource)
.Append(headerSource);
AppendInlineRequestProperties(builder, request, interfaceModel, methodModel, requestLocal, settingsLocal);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,12 +207,14 @@ internal static string GetParametersArg(
replacements.Sort(static (left, right) => left.Start.CompareTo(right.Start));

var parametersSb = new PooledStringBuilder();
var indent = Indent(MethodBodyIndentation + 1 + 1);

var first = true;
foreach (var replacement in replacements)
{
if (!first)
{
_ = parametersSb.Append(", ");
_ = parametersSb.AppendLine(",").Append(indent);
}

first = false;
Expand Down
77 changes: 63 additions & 14 deletions src/InterfaceStubGenerator.Shared/Emitter.Inline.Path.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ internal static partial class Emitter
/// <c>ReadOnlySpan</c> overload accepts via the array-to-span conversion. The array element type is inferred from the
/// tuple values rather than stated, so no nullable reference annotation is emitted into a pre-C# 8 consumer.</remarks>
internal static string WrapPathReplacements(string tuples, bool supportsCollectionExpressions) =>
supportsCollectionExpressions ? $", [{tuples}]" : $", new[] {{ {tuples} }}";
supportsCollectionExpressions ? $"[{tuples}]" : $"new[] {{ {tuples} }}";

/// <summary>Determines whether any path parameter passes its value through pre-encoded.</summary>
/// <param name="request">The parsed request model.</param>
Expand Down Expand Up @@ -74,10 +74,32 @@ internal static string BuildInlinePathExpression(
{
// A template with placeholders but no bound path parameters still runs the unmatched-placeholder
// check so AllowUnmatchedRouteParameters keeps its reflection-path semantics.
return TryBuildInlinePathFastExpression(request, parameterInfoNames, emission)
?? (parameters.Length > 0 || request.Path.IndexOf('{') >= 0
? $"global::Refit.GeneratedRequestRunner.BuildRequestPath({ToCSharpStringLiteral(request.Path)}, {settingsLocal}.AllowUnmatchedRouteParameters{parameters})"
: ToCSharpStringLiteral(request.Path));
var indent = Indent(MethodBodyIndentation + 1 + 1);
var expression = TryBuildInlinePathFastExpression(request, parameterInfoNames, emission);
if (expression is not null)
{
return expression;
}

if (parameters.Length > 0 || request.Path.IndexOf('{') >= 0)
{
var stringBuilder = new PooledStringBuilder()
.AppendLine("global::Refit.GeneratedRequestRunner.BuildRequestPath(")
.Append(indent).Append(ToCSharpStringLiteral(request.Path)).AppendLine(",")
.Append(indent).Append(settingsLocal).Append(".AllowUnmatchedRouteParameters");

if (parameters.Length > 0)
{
_ = stringBuilder
.AppendLine(",")
.Append(indent)
.Append(parameters);
}

return stringBuilder.Append(')').ToString();
}

return ToCSharpStringLiteral(request.Path);
}

/// <summary>Builds the allocation-free path expression for a single span-formattable path parameter, or null.</summary>
Expand Down Expand Up @@ -122,20 +144,47 @@ internal static string BuildInlinePathExpression(
var start = location.Start.GetOffset(pathLength);
var end = location.End.GetOffset(pathLength);
var template = ToCSharpStringLiteral(request.Path);
var settingsLocal = emission.SettingsLocal;
var allowUnmatched = $"{settingsLocal}.AllowUnmatchedRouteParameters";

var allowUnmatched = $"{emission.SettingsLocal}.AllowUnmatchedRouteParameters";
var valueExpression = $"@{pathParameter.Value.Name}";
_ = parameterInfoNames.TryGetValue(pathParameter.Value.Name, out var providerField);
const string runner = "global::Refit.GeneratedRequestRunner.BuildRequestPath";

var fastExpression = valueFormat.IsUrlSafeSpanFormattable
? $"{runner}({template}, {allowUnmatched}, ({start}, {end}), {valueExpression})"
: $"{runner}({template}, {allowUnmatched}, ({start}, {end}), {valueExpression}, {ToNullableCSharpStringLiteral(valueFormat.Format)})";
var indentBuildRelativeUriParameter = Indent(MethodBodyIndentation + 1);
var indentBuilderBuildRequestPath = Indent(MethodBodyIndentation + 1 + 1);
var stringBuilder = new PooledStringBuilder().Append('(').AppendLine(emission.UseDefaultFormattingLocal)
.Append(indentBuildRelativeUriParameter).Append("? ");
_ = BuildCommonInlinePathFastExpression(stringBuilder, indentBuilderBuildRequestPath, template, allowUnmatched)
.Append('(').Append(start).Append(", ").Append(end).AppendLine("),")
.Append(indentBuilderBuildRequestPath).Append(valueExpression);
if (!valueFormat.IsUrlSafeSpanFormattable)
{
_ = stringBuilder
.AppendLine(",")
.Append(indentBuilderBuildRequestPath).Append(ToNullableCSharpStringLiteral(valueFormat.Format));
}

var customTuple =
$"(({start}, {end}), {EmitFormatUrlParameter(valueExpression, providerField, $"typeof({pathParameter.Value.Type})", emission)})";
var customReplacements = WrapPathReplacements(customTuple, emission.SupportsCollectionExpressions);
var customExpression = $"{runner}({template}, {allowUnmatched}{customReplacements})";
_ = stringBuilder
.AppendLine(")")
.Append(indentBuildRelativeUriParameter).Append(": ");
return BuildCommonInlinePathFastExpression(stringBuilder, indentBuilderBuildRequestPath, template, allowUnmatched)
.Append(WrapPathReplacements(customTuple, emission.SupportsCollectionExpressions)).Append("))").ToString();
}

return $"({emission.UseDefaultFormattingLocal} ? {fastExpression} : {customExpression})";
/// <summary>Writes the shared inline path-building helper call for generated request path emission.</summary>
/// <param name="builder">The pooled string builder receiving the generated source.</param>
/// <param name="indentBuilderBuildRequestPath">The indentation prefix used for the helper call arguments.</param>
/// <param name="template">The escaped path template literal emitted into the generated code.</param>
/// <param name="allowUnmatched">The expression that controls whether unmatched route parameters are allowed.</param>
/// <returns>The same pooled string builder, allowing chained emission.</returns>
internal static PooledStringBuilder BuildCommonInlinePathFastExpression(PooledStringBuilder builder, string indentBuilderBuildRequestPath, string template, string allowUnmatched)
{
const string runner = "global::Refit.GeneratedRequestRunner.BuildRequestPath";
return builder
.Append(runner).AppendLine("(")
.Append(indentBuilderBuildRequestPath).Append(template).AppendLine(",")
.Append(indentBuilderBuildRequestPath).Append(allowUnmatched).AppendLine(",")
.Append(indentBuilderBuildRequestPath);
}
}
21 changes: 17 additions & 4 deletions src/InterfaceStubGenerator.Shared/Emitter.Inline.cs
Original file line number Diff line number Diff line change
Expand Up @@ -589,10 +589,23 @@ internal static bool HasUrlParameter(in RequestModel request) =>
/// <returns>The generated <c>BuildRelativeUri</c> call.</returns>
/// <remarks>A <c>[QueryUriFormat]</c> method re-encodes the whole path and query with the attribute's UriFormat,
/// matching the reflection builder's final GetComponents pass; every other method uses the direct relative URI.</remarks>
internal static string BuildRelativeUriExpression(in RequestModel request, string requestPathExpression, string settingsLocal) =>
request.QueryUriFormat is { } queryUriFormat
? $"global::Refit.GeneratedRequestRunner.BuildRelativeUri(this.Client, {requestPathExpression}, {settingsLocal}.UrlResolution, (global::System.UriFormat){queryUriFormat})"
: $"global::Refit.GeneratedRequestRunner.BuildRelativeUri(this.Client, {requestPathExpression}, {settingsLocal}.UrlResolution)";
internal static string BuildRelativeUriExpression(in RequestModel request, string requestPathExpression, string settingsLocal)
{
var bodyAndExtraIndent = Indent(MethodBodyIndentation + 1);
var stringBuilder = new PooledStringBuilder()
.AppendLine("global::Refit.GeneratedRequestRunner.BuildRelativeUri(")
.Append(bodyAndExtraIndent).AppendLine("this.Client,")
.Append(bodyAndExtraIndent).Append(requestPathExpression).AppendLine(",")
.Append(bodyAndExtraIndent).Append(settingsLocal).Append(".UrlResolution");
if (request.QueryUriFormat is { } queryUriFormat)
{
_ = stringBuilder
.AppendLine(",")
.Append(bodyAndExtraIndent).Append("(global::System.UriFormat)").Append(queryUriFormat);
}

return stringBuilder.Append(')').ToString();
}

/// <summary>Finds the first request parameter of the given kind.</summary>
/// <param name="request">The request model to inspect.</param>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,8 @@ public async Task SwitchOnGeneratesInlineForQueryUriFormat()
generatedRequestBuilding: true);

await Assert.That(generated).DoesNotContain(ReflectiveRequestBuilderCall);
await Assert.That(generated).Contains(".UrlResolution, (global::System.UriFormat)");
await Assert.That(generated).Contains(".UrlResolution,");
await Assert.That(generated).Contains("(global::System.UriFormat)");
}

/// <summary>Verifies custom HTTP method attributes are discovered but fall back to the runtime builder.</summary>
Expand Down Expand Up @@ -113,7 +114,7 @@ public interface IGeneratedClient

// The custom verb is allocated once in a static field and the request references it, not a per-call allocation.
await Assert.That(generated).Contains("private static readonly global::System.Net.Http.HttpMethod ______httpMethod = new global::System.Net.Http.HttpMethod(\"PURGE\");");
await Assert.That(generated).Contains("new global::System.Net.Http.HttpRequestMessage(______httpMethod,");
await Assert.That(generated).Contains("______httpMethod,");
}

/// <summary>Verifies a custom HTTP QUERY verb attribute (a draft-standard body-carrying method) with an explicit
Expand Down Expand Up @@ -218,7 +219,7 @@ public async Task SwitchOnRemovesWhitespaceAndEmptyQueryKeysFromInlineConstantPa
GeneratedClientHintName,
generatedRequestBuilding: true);

await Assert.That(generated).Contains("BuildRelativeUri(this.Client, \"/foo?one=1&two\"");
await Assert.That(generated).Contains("\"/foo?one=1&two\"");
await Assert.That(generated).DoesNotContain("drop");
await Assert.That(generated).DoesNotContain(ReflectiveRequestBuilderCall);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,9 @@ public interface IGeneratedClient
var generated = result.GeneratedSources[GeneratedClientHintName];

await Assert.That(result.CompilesWithoutErrors).IsTrue();
await Assert.That(generated).Contains("""GeneratedRequestRunner.BuildRequestPath("/a/{aVal}", refitSettings.AllowUnmatchedRouteParameters, [((3, 9), """);
await Assert.That(generated).Contains(""" "/a/{aVal}",""");
await Assert.That(generated).Contains("refitSettings.AllowUnmatchedRouteParameters,");
await Assert.That(generated).Contains("[((3, 9), ");
}

/// <summary>Verifies that path parameters are supported by the source generator.</summary>
Expand All @@ -330,7 +332,7 @@ public interface IGeneratedClient
var generated = result.GeneratedSources[GeneratedClientHintName];

await Assert.That(result.CompilesWithoutErrors).IsTrue();
await Assert.That(generated).Contains("""GeneratedRequestRunner.BuildRequestPath("/a?b={bVal}", refitSettings.AllowUnmatchedRouteParameters, [((5, 11), """);
await Assert.That(generated).Contains("[((5, 11), ");
}

/// <summary>Verifies that auto-appended query parameters generate inline query construction.</summary>
Expand Down
12 changes: 6 additions & 6 deletions src/tests/Refit.GeneratorTests/GeneratedRequestBuildingTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,7 +210,7 @@ public async Task SwitchOnStripsFragmentsFromInlineConstantPaths()
GeneratedClientHintName,
generatedRequestBuilding: true);

await Assert.That(generated).Contains("BuildRelativeUri(this.Client, \"/foo?key=value\"");
await Assert.That(generated).Contains("\"/foo?key=value\",");
await Assert.That(generated).DoesNotContain("#name");
await Assert.That(generated).DoesNotContain(ReflectiveRequestBuilderCall);
}
Expand All @@ -228,7 +228,7 @@ public async Task SwitchOnStripsQueryAfterFragmentFromInlineConstantPaths()
GeneratedClientHintName,
generatedRequestBuilding: true);

await Assert.That(generated).Contains("BuildRelativeUri(this.Client, \"/foo\"");
await Assert.That(generated).Contains("\"/foo\",");
await Assert.That(generated).DoesNotContain("?key=value");
await Assert.That(generated).DoesNotContain(ReflectiveRequestBuilderCall);
}
Expand All @@ -246,7 +246,7 @@ public async Task SwitchOnRemovesEmptyQueryKeysFromInlineConstantPaths()
GeneratedClientHintName,
generatedRequestBuilding: true);

await Assert.That(generated).Contains("BuildRelativeUri(this.Client, \"/foo?key=&two=2\"");
await Assert.That(generated).Contains("\"/foo?key=&two=2\",");
await Assert.That(generated).DoesNotContain("=drop");
await Assert.That(generated).DoesNotContain(ReflectiveRequestBuilderCall);
}
Expand Down Expand Up @@ -363,9 +363,9 @@ public interface IGeneratedClient
GeneratedClientHintName,
generatedRequestBuilding: true);

await Assert.That(generated).Contains("BuildRelativeUri(this.Client, \"/global\"");
await Assert.That(generated).Contains("BuildRelativeUri(this.Client, \"/alias\"");
await Assert.That(generated).Contains("BuildRelativeUri(this.Client, \"/qualified\"");
await Assert.That(generated).Contains("\"/global\",");
await Assert.That(generated).Contains("\"/alias\",");
await Assert.That(generated).Contains("\"/qualified\",");
await Assert.That(generated).Contains("HttpMethod.Get");
await Assert.That(generated).Contains("HttpMethod.Post");
await Assert.That(generated).Contains("HttpMethod.Put");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ public async Task GeneratedInlinesNoLeadingSlashPath()
{
var generated = string.Concat(Fixture.RunGenerator(ApiSource, generatedRequestBuilding: true).GeneratedSources.Values);

await Assert.That(generated).Contains("BuildRequestPath(\"relative/{id}\"");
await Assert.That(generated).Contains("\"relative/{id}\",");
await Assert.That(generated).DoesNotContain("BuildRestResultFuncForMethod(\"Relative\"");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@ public RefitGeneratorTestIGeneratedClient(global::System.Net.Http.HttpClient cli
public global::System.Threading.Tasks.Task<string> Get()
{
var refitSettings = _settings;
var refitRequest = new global::System.Net.Http.HttpRequestMessage(global::System.Net.Http.HttpMethod.Get, global::Refit.GeneratedRequestRunner.BuildRelativeUri(this.Client, "/users", refitSettings.UrlResolution));
var refitRequest = new global::System.Net.Http.HttpRequestMessage(
global::System.Net.Http.HttpMethod.Get,
global::Refit.GeneratedRequestRunner.BuildRelativeUri(
this.Client,
"/users",
refitSettings.UrlResolution));
global::Refit.GeneratedRequestRunner.AddConfiguredRequestOptions(refitRequest, refitSettings, typeof(global::RefitGeneratorTest.IGeneratedClient));
global::Refit.GeneratedRequestRunner.AddRequestProperty<string>(refitRequest, global::Refit.HttpRequestMessageOptions.MethodName, "Get");
global::Refit.GeneratedRequestRunner.AddRequestProperty<string>(refitRequest, global::Refit.HttpRequestMessageOptions.RelativePathTemplate, "/users");
Expand Down
Loading
Loading