Skip to content

Commit fc496b0

Browse files
author
Javier García de la Noceda Argüelles
committed
Handle PR comments
1 parent 7e6fb77 commit fc496b0

4 files changed

Lines changed: 104 additions & 48 deletions

File tree

src/InterfaceStubGenerator.Shared/Parser.Request.Query.cs

Lines changed: 38 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ internal static partial class Parser
1717
/// <summary>The maximum nested-object depth flattened inline before the whole parameter falls back to reflection.</summary>
1818
private const int MaxNestingDepth = 32;
1919

20-
/// <summary>The underlying integer value of <c>CollectionFormat.Indexed</c>, used by the parser without a direct reference to the Refit assembly.</summary>
21-
private const int IndexedCollectionFormatValue = 6;
20+
/// <summary>The metadata name of <c>Refit.CollectionFormat</c>.</summary>
21+
private const string CollectionFormatTypeName = "Refit.CollectionFormat";
22+
23+
/// <summary>The member name of <c>CollectionFormat.Indexed</c>.</summary>
24+
private const string IndexedMemberName = "Indexed";
2225

2326
/// <summary>The metadata name of <c>Refit.QueryAttribute</c>.</summary>
2427
private const string QueryAttributeDisplayName = "QueryAttribute";
@@ -286,6 +289,38 @@ internal static bool TryBuildQueryModel(
286289
BuildValueFormat(elementType!, format, formattableSymbol, context))
287290
: null;
288291

292+
/// <summary>Determines whether <paramref name="collectionFormatValue"/> is the underlying integer value of
293+
/// <c>CollectionFormat.Indexed</c> by resolving the enum member from the compilation rather than comparing
294+
/// against a hardcoded literal, so the check stays correct if the enum is ever reordered.</summary>
295+
/// <param name="collectionFormatValue">The collection format value from the attribute, or null.</param>
296+
/// <param name="context">The generation context supplying the compilation.</param>
297+
/// <returns><see langword="true"/> when the value matches <c>CollectionFormat.Indexed</c>.</returns>
298+
internal static bool IsIndexedCollectionFormat(int? collectionFormatValue, in InterfaceGenerationContext context)
299+
{
300+
if (collectionFormatValue is null)
301+
{
302+
return false;
303+
}
304+
305+
// Resolve the CollectionFormat enum type from the compilation and find the Indexed member's constant value.
306+
// This avoids hardcoding the integer and stays correct if the enum is ever reordered.
307+
var collectionFormatType = context.Compilation?.GetTypeByMetadataName(CollectionFormatTypeName);
308+
if (collectionFormatType is null)
309+
{
310+
return false;
311+
}
312+
313+
foreach (var member in collectionFormatType.GetMembers())
314+
{
315+
if (member is IFieldSymbol { HasConstantValue: true, Name: IndexedMemberName } field)
316+
{
317+
return collectionFormatValue == (int)field.ConstantValue!;
318+
}
319+
}
320+
321+
return false;
322+
}
323+
289324
/// <summary>Builds the query model for a <c>[Query(CollectionFormat.Indexed)]</c> parameter whose element type
290325
/// is a complex object that can be flattened inline, or null when the parameter does not match.</summary>
291326
/// <param name="parameter">The parameter to classify.</param>
@@ -306,7 +341,7 @@ internal static bool TryBuildQueryModel(
306341
INamedTypeSymbol? formattableSymbol,
307342
in InterfaceGenerationContext context)
308343
{
309-
if (data.CollectionFormatValue != IndexedCollectionFormatValue)
344+
if (!IsIndexedCollectionFormat(data.CollectionFormatValue, context))
310345
{
311346
return null;
312347
}

src/Refit.Reflection/RequestBuilderImplementation.Payload.cs

Lines changed: 38 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -43,41 +43,41 @@ internal void AddBodyToRequest(RestMethodInfoInternal restMethod, object param,
4343
switch (restMethod.BodyParameterInfo.Item1)
4444
{
4545
case BodySerializationMethod.UrlEncoded:
46-
{
47-
ret.Content = param is string str
48-
? new StringContent(
49-
StringHelpers.EscapeDataString(str),
50-
Encoding.UTF8,
51-
"application/x-www-form-urlencoded")
52-
: new FormUrlEncodedContent(new FormValueMultimap(param, _settings));
53-
break;
54-
}
46+
{
47+
ret.Content = param is string str
48+
? new StringContent(
49+
StringHelpers.EscapeDataString(str),
50+
Encoding.UTF8,
51+
"application/x-www-form-urlencoded")
52+
: new FormUrlEncodedContent(new FormValueMultimap(param, _settings));
53+
break;
54+
}
5555

5656
case BodySerializationMethod.JsonLines:
57-
{
58-
ret.Content = new JsonLinesContent(AsJsonLinesSequence(param), _serializer);
59-
break;
60-
}
57+
{
58+
ret.Content = new JsonLinesContent(AsJsonLinesSequence(param), _serializer);
59+
break;
60+
}
6161

6262
case BodySerializationMethod.Default or BodySerializationMethod.Serialized:
63-
{
64-
AddSerializedBodyToRequest(restMethod, param, ret);
65-
break;
66-
}
63+
{
64+
AddSerializedBodyToRequest(restMethod, param, ret);
65+
break;
66+
}
6767

6868
default:
69+
{
70+
// The obsolete legacy JSON serialization method must still serialize, because
71+
// already-compiled callers can pass it. Treating it as Default would incorrectly
72+
// send string bodies as raw text. It is matched by value rather than by name so
73+
// this file never references the obsolete member.
74+
if (GeneratedRequestRunner.IsObsoleteJsonSerializationMethod(restMethod.BodyParameterInfo.Item1))
6975
{
70-
// The obsolete legacy JSON serialization method must still serialize, because
71-
// already-compiled callers can pass it. Treating it as Default would incorrectly
72-
// send string bodies as raw text. It is matched by value rather than by name so
73-
// this file never references the obsolete member.
74-
if (GeneratedRequestRunner.IsObsoleteJsonSerializationMethod(restMethod.BodyParameterInfo.Item1))
75-
{
76-
AddSerializedBodyToRequest(restMethod, param, ret);
77-
}
78-
79-
break;
76+
AddSerializedBodyToRequest(restMethod, param, ret);
8077
}
78+
79+
break;
80+
}
8181
}
8282
}
8383

@@ -423,7 +423,8 @@ internal bool TryAppendIndexedCollectionQuery(
423423
if (parameterCollectionFormat != CollectionFormat.Indexed
424424
|| param is string
425425
|| param is IDictionary
426-
|| param is not IEnumerable collection)
426+
|| param is not IEnumerable collection
427+
|| DoNotConvertToQueryMap(param))
427428
{
428429
return false;
429430
}
@@ -454,23 +455,16 @@ internal void AppendIndexedCollectionParameters(
454455
}
455456

456457
var indexedKey = $"{paramKey}[{index}]";
457-
if (DoNotConvertToQueryMap(item))
458-
{
459-
var type = item.GetType();
460-
queryParamsToAdd.Add(new(indexedKey, GeneratedRequestRunner.FormatUrlParameter(_settings, item, type, type), KeyPreEscaped: true));
461-
}
462-
else
458+
459+
var queryMap = BuildQueryMap(item, attr.Delimiter);
460+
for (var j = 0; j < queryMap.Count; j++)
463461
{
464-
var queryMap = BuildQueryMap(item, attr.Delimiter);
465-
for (var j = 0; j < queryMap.Count; j++)
466-
{
467-
var kvp = queryMap[j];
468-
var valueType = kvp.Value?.GetType() ?? typeof(object);
469-
queryParamsToAdd.Add(new(
470-
indexedKey + attr.Delimiter + kvp.Key,
471-
GeneratedRequestRunner.FormatUrlParameter(_settings, kvp.Value, valueType, valueType),
472-
KeyPreEscaped: true));
473-
}
462+
var kvp = queryMap[j];
463+
var valueType = kvp.Value?.GetType() ?? typeof(object);
464+
queryParamsToAdd.Add(new(
465+
indexedKey + attr.Delimiter + kvp.Key,
466+
GeneratedRequestRunner.FormatUrlParameter(_settings, kvp.Value, valueType, valueType),
467+
KeyPreEscaped: true));
474468
}
475469

476470
index++;

src/tests/Refit.GeneratorTests/QueryRequestBuildingLiveTests.Helpers.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ private sealed class LiveQueryHarness(
8383
using System.Collections.Generic;
8484
using System.Runtime.Serialization;
8585
using System.Threading.Tasks;
86+
using System.Text.Json.Serialization;
8687
using Refit;
8788
8889
namespace Refit.LiveQuery;
@@ -173,6 +174,12 @@ public sealed class Item
173174
174175
public string? Value { get; set; }
175176
}
177+
public sealed class Name
178+
{
179+
[JsonPropertyName("First Name")]
180+
public string? FirstName { get; set; }
181+
public string? LastName { get; set; }
182+
}
176183
177184
public interface ILiveQueryApi
178185
{
@@ -275,6 +282,12 @@ public interface ILiveQueryApi
275282
276283
[Get("/indexed")]
277284
Task<string> IndexedSearch([Query(CollectionFormat.Indexed)] List<Item>? items);
285+
286+
[Get("/indexedListInt")]
287+
Task<string> IndexedListSearch([Query(CollectionFormat.Indexed)] List<int>? items);
288+
289+
[Get("/indexedNameWithSerialized")]
290+
Task<string> indexedNameWithSerialized([Query(CollectionFormat.Indexed)] List<Name>? items);
278291
}
279292
""";
280293

src/tests/Refit.GeneratorTests/QueryRequestBuildingLiveTests.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -278,7 +278,21 @@ public async Task IndexedCollectionQueryParametersMatchReflection()
278278
await harness.AssertParityAsync(indexedSearchMethodName, [null], "/base/indexed");
279279
var item0 = harness.CreateApiValue(typeName, (id, 1), (value, "a"));
280280
var indexedList = harness.CreateApiList(typeName, item0);
281-
_ = await harness.AssertParityAsync(indexedSearchMethodName, [indexedList], $"/base/indexed?{parameter}[0].{id}=1&{parameter}[0].{value}=a");
281+
await harness.AssertParityAsync(indexedSearchMethodName, [indexedList], $"/base/indexed?{parameter}[0].{id}=1&{parameter}[0].{value}=a");
282+
283+
const string indexedSearchWithListIntMethodName = "IndexedListSearch";
284+
await harness.AssertParityAsync(indexedSearchWithListIntMethodName, [null], "/base/indexedListInt");
285+
List<int> values = [1, 2];
286+
await harness.AssertParityAsync(indexedSearchWithListIntMethodName, [values], $"/base/indexedListInt?{parameter}=1%2C2");
287+
288+
const string name = "FirstName";
289+
const string jsonPropertyName = "First%20Name";
290+
const string lastName = "LastName";
291+
const string indexedSearchSerialized = "indexedNameWithSerialized";
292+
const string typeNameSerialized = "Refit.LiveQuery.Name";
293+
var nameSerialized = harness.CreateApiValue(typeNameSerialized, (name, "John"), (lastName, "Doe"));
294+
var indexedNameSerialized = harness.CreateApiList(typeNameSerialized, nameSerialized);
295+
await harness.AssertParityAsync(indexedSearchSerialized, [indexedNameSerialized], $"/base/indexedNameWithSerialized?{parameter}[0].{jsonPropertyName}=John&{parameter}[0].{lastName}=Doe");
282296
}
283297

284298
/// <summary>Verifies a custom URL parameter formatter still runs for every generated value.</summary>

0 commit comments

Comments
 (0)