Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,7 +357,7 @@ public interface ISomeApi

Sometimes you have a load of query parameters, or they're generated dynamically, etc. In this case, you may want to supply a dictionary of query parameters, rather than specifying a load of method parameters.

To facilitate this, you may decorate one or more method parameters with `[QueryMap]`. The parameter type must be an `IDictionary<TKey, TValue>`.
To facilitate this, you may decorate one or more method parameters with `[QueryMap]`. The parameter type must be an `IEnumerable<KeyValuePair<TKey, TValue>>`(`IDictionary<,>` already inherited).

Query maps are handled the same way as other query parameters: serialization, handling of enumerables, null values, etc, behave the same. You can control whether values are serialized using a custom serializer or `ToString()` using e.g. `[QueryMap(QuerySerializationMethod.Serialized)]`.

Expand All @@ -367,9 +367,9 @@ For example:
public interface ISomeApi
{
[Get("search")]
// I've used IDictionary<string, string[]> here, but you can use whatever type parameters you like,
// or any type which implements IDictionary<TKey, TValue>
Task<SearchResult> SearchBlogPostsAsync([QueryMap] IDictionary<string, string[]> filters);
// I've used Dictionary<string, string[]> here, but you can use whatever type parameters you like,
// or any type which implements IEnumerable<KeyValuePair<TKey, TValue>>
Task<SearchResult> SearchBlogPostsAsync([QueryMap] Dictionary<string, string[]> filters);
}

var api = RestClient.For<ISomeApi>("https://api.example.com");
Expand Down
2 changes: 1 addition & 1 deletion src/Common/Implementation/DiagnosticCode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public enum DiagnosticCode
HeaderMustNotHaveColonInName = 10,
PropertyMustBeReadWrite = 11,
HeaderPropertyWithValueMustBeNullable = 12,
QueryMapParameterIsNotADictionary = 13,
QueryMapParameterIsNotKeyValuePairs = 13,
AllowAnyStatusCodeAttributeNotAllowedOnParentInterface = 14,
EventsNotAllowed = 15,
PropertyMustBeReadOnly = 16,
Expand Down
4 changes: 2 additions & 2 deletions src/Common/Implementation/Emission/DiagnosticReporter.Emit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,8 +235,8 @@ public void ReportMultipleBodyParameters(MethodModel method, IEnumerable<Paramet
public void ReportQueryMapParameterIsNotADictionary(MethodModel method, ParameterModel _)
{
throw new ImplementationCreationException(
DiagnosticCode.QueryMapParameterIsNotADictionary,
$"Method '{method.MethodInfo.Name}': [QueryMap] parameter is not of type IDictionary or IDictionary<TKey, TValue> (or one of their descendents)");
DiagnosticCode.QueryMapParameterIsNotKeyValuePairs,
$"Method '{method.MethodInfo.Name}': [QueryMap] parameter is not of type IEnumerable<KeyValuePair<TKey, TValue>> (or one of its descendents)");
}

public void ReportHeaderParameterMustNotHaveValue(MethodModel method, ParameterModel parameter)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -315,9 +315,9 @@ public void ReportMultipleBodyParameters(MethodModel _, IEnumerable<ParameterMod
}

private static readonly DiagnosticDescriptor queryMapParameterIsNotADictionary = CreateDescriptor(
DiagnosticCode.QueryMapParameterIsNotADictionary,
DiagnosticCode.QueryMapParameterIsNotKeyValuePairs,
"QueryMap parameters must be dictionaries",
"[QueryMap] parameter is not of the type IDictionary or IDictionary<TKey, TValue> (or their descendents)");
"[QueryMap] parameter is not of the type IEnumerable<KeyValuePair<TKey, TValue>> (or its descendents)");
public void ReportQueryMapParameterIsNotADictionary(MethodModel _, ParameterModel parameter)
{
this.AddDiagnostic(queryMapParameterIsNotADictionary, SymbolLocations(parameter.ParameterSymbol));
Expand Down
16 changes: 10 additions & 6 deletions src/Common/Implementation/Emission/MethodEmitter.Emit.cs
Original file line number Diff line number Diff line change
Expand Up @@ -401,7 +401,7 @@ private static MethodInfo MakeQueryParameterMethodInfo(Type parameterType)

private static MethodInfo? MakeQueryMapMethodInfo(Type queryMapType)
{
var nullableDictionaryTypes = DictionaryTypesOfType(queryMapType);
var nullableDictionaryTypes = EnumerableKeyValueTypesOfType(queryMapType);
if (nullableDictionaryTypes == null)
return null;

Expand All @@ -424,14 +424,18 @@ private static MethodInfo MakeQueryParameterMethodInfo(Type parameterType)
}
}

private static KeyValuePair<Type, Type>? DictionaryTypesOfType(Type input)
private static KeyValuePair<Type, Type>? EnumerableKeyValueTypesOfType(Type input)
{
foreach (var baseType in EnumerableExtensions.Concat(input, input.GetTypeInfo().GetInterfaces()))
{
if (baseType.GetTypeInfo().IsGenericType && baseType.GetGenericTypeDefinition() == typeof(IDictionary<,>))
if (baseType.GetTypeInfo().IsGenericType && baseType.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
var genericArguments = baseType.GetTypeInfo().GetGenericArguments();
return new KeyValuePair<Type, Type>(genericArguments[0], genericArguments[1]);
var enumeratedType = baseType.GetTypeInfo().GetGenericArguments()[0].GetTypeInfo();
if (enumeratedType.IsGenericType && enumeratedType.GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
{
var genericArguments = enumeratedType.GetGenericArguments();
return new KeyValuePair<Type, Type>(genericArguments[0], genericArguments[1]);
}
}
}

Expand Down Expand Up @@ -467,4 +471,4 @@ private static void Assert([DoesNotReturnIf(false)] bool condition)
Debug.Assert(condition);
}
}
}
}
45 changes: 37 additions & 8 deletions src/Common/Implementation/Emission/MethodEmitter.Roslyn.cs
Original file line number Diff line number Diff line change
Expand Up @@ -339,7 +339,7 @@ private static string EnumValue<T>(T value) where T : struct, Enum

private string? GetQueryMapMethodName(ITypeSymbol queryMapType)
{
var nullableDictionaryTypes = this.DictionaryTypesOfType(queryMapType);
var nullableDictionaryTypes = this.EnumerableKeyValueTypesOfType(queryMapType);
if (nullableDictionaryTypes == null)
return null;

Expand All @@ -360,18 +360,21 @@ private static string EnumValue<T>(T value) where T : struct, Enum
}
}

private KeyValuePair<ITypeSymbol, ITypeSymbol>? DictionaryTypesOfType(ITypeSymbol input)
private KeyValuePair<ITypeSymbol, ITypeSymbol>? EnumerableKeyValueTypesOfType(ITypeSymbol input)
{
KeyValuePair<ITypeSymbol, ITypeSymbol>? result = null;
if (input is INamedTypeSymbol value)
{
foreach (var baseType in value.AllInterfaces.Prepend(value))
{
if (baseType.IsGenericType && SymbolEqualityComparer.Default.Equals(baseType.ConstructedFrom, this.wellKnownSymbols.IDictionaryKV))
if (baseType.IsGenericType && SymbolEqualityComparer.Default.Equals(baseType.ConstructedFrom, this.wellKnownSymbols.IEnumerableT))
{
result = new KeyValuePair<ITypeSymbol, ITypeSymbol>(
baseType.TypeArguments[0], baseType.TypeArguments[1]);
break;
var enumeratedType = baseType.TypeArguments[0];
result = this.KeyValueTypesOfType(enumeratedType);
if (result != null)
{
break;
}
}
}
}
Expand All @@ -380,7 +383,33 @@ private static string EnumValue<T>(T value) where T : struct, Enum
// Are any of its constraints suitable dictionaries
foreach (var constraint in typeParameter.ConstraintTypes)
{
result = this.DictionaryTypesOfType(constraint);
result = this.EnumerableKeyValueTypesOfType(constraint);
if (result != null)
{
break;
}
}
}

return result;
}

private KeyValuePair<ITypeSymbol, ITypeSymbol>? KeyValueTypesOfType(ITypeSymbol input)
{
KeyValuePair<ITypeSymbol, ITypeSymbol>? result = null;
if (input is INamedTypeSymbol namedType)
{
if (namedType.IsGenericType && SymbolEqualityComparer.Default.Equals(namedType.ConstructedFrom, this.wellKnownSymbols.KeyValuePairKV))
{
result = new KeyValuePair<ITypeSymbol, ITypeSymbol>(namedType.TypeArguments[0], namedType.TypeArguments[1]);
}
}
else if (input is ITypeParameterSymbol typeParameter)
{
// Are any of its constraints suitable KeyValuePair
foreach (var constraint in typeParameter.ConstraintTypes)
{
result = this.KeyValueTypesOfType(constraint);
if (result != null)
{
break;
Expand Down Expand Up @@ -429,4 +458,4 @@ private static string EnumValue<T>(T value) where T : struct, Enum
return collectionType;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ internal class WellKnownSymbols
private INamedTypeSymbol? ienumerableT;
public INamedTypeSymbol? IEnumerableT => this.ienumerableT ??= this.LookupKnownSystem("System.Collections.Generic.IEnumerable`1");

private INamedTypeSymbol? idictionaryKV;
public INamedTypeSymbol? IDictionaryKV => this.idictionaryKV ??= this.LookupKnownSystem("System.Collections.Generic.IDictionary`2");
private INamedTypeSymbol? keyValuePairKV;
public INamedTypeSymbol? KeyValuePairKV => this.keyValuePairKV ??= this.LookupKnownSystem("System.Collections.Generic.KeyValuePair`2");

private INamedTypeSymbol? httpMethod;
public INamedTypeSymbol? HttpMethod => this.httpMethod ??= this.LookupKnownSystem("System.Net.Http.HttpMethod");
Expand Down
123 changes: 121 additions & 2 deletions src/RestEase.UnitTests/ImplementationFactoryTests/QueryMapTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,43 @@ public interface IHasGenericCollectionQueryMap
Task FooAsync<TCollection>([QueryMap] IDictionary<string, TCollection> map) where TCollection : IEnumerable<string>;
}

public interface IHasIEnumerableKeyValuePairsQueryMap
{
[Get("foo")]
Task FooAsync([QueryMap] IEnumerable<KeyValuePair<string, string>> map);
}

public interface IHasIEnumerableGenericKeyValuePairsQueryMap
{
[Get("foo")]
Task FooAsync<TKey, TValue>([QueryMap] IEnumerable<KeyValuePair<TKey, TValue>> map)
where TKey : class
where TValue : IList<string>;
}

public interface IHasGenericIEnumerableKeyValuePairsQueryMap
{
[Get("foo")]
Task FooAsync<TEnumerable>([QueryMap] TEnumerable map) where TEnumerable : IEnumerable<KeyValuePair<string, string>>;
}

public interface IHasGenericIEnumerableGenericKeyValuePairsQueryMap
{
[Get("foo")]
Task FooAsync<TEnumerable, TKey, TValue>([QueryMap] TEnumerable map)
where TEnumerable : IEnumerable<KeyValuePair<TKey, TValue>>
where TValue : IList<string>;
}

public interface IHasGenericIEnumerableGenericKeyGenericListValueQueryMap
{
[Get("foo")]
Task FooAsync<TEnumerable, TKey, TValue, TItem>([QueryMap] TEnumerable map)
where TEnumerable : IEnumerable<KeyValuePair<TKey, TValue>>
where TValue : IList<TItem>
where TItem : class;
}

public QueryMapTests(ITestOutputHelper output) : base(output) { }

[Fact]
Expand Down Expand Up @@ -184,9 +221,9 @@ public void HandlesNullQueryMap()
public void ThrowsIfInvalidQueryMapType()
{
this.VerifyDiagnostics<IHasInvalidQueryMap>(
// (4,27): Error REST013: [QueryMap] parameter is not of the type IDictionary or IDictionary<TKey, TValue> (or their descendents)
// (4,27): Error REST013: [QueryMap] parameter is not of the type IEnumerable<KeyValuePair<TKey, TValue>> (or its descendents)
// [QueryMap] string map
Diagnostic(DiagnosticCode.QueryMapParameterIsNotADictionary, "[QueryMap] string map").WithLocation(4, 27)
Diagnostic(DiagnosticCode.QueryMapParameterIsNotKeyValuePairs, "[QueryMap] string map").WithLocation(4, 27)
);
}

Expand Down Expand Up @@ -312,5 +349,87 @@ public void SerializeToStringUsesGivenFormatProvider()

formatProvider.Verify(x => x.GetFormat(typeof(NumberFormatInfo)));
}

[Fact]
public void HandlesIEnumerableKeyValuePairs()
{
var pair = new KeyValuePair<string, string>("k1", "v1");
var queryMap = new[]{ pair }.AsEnumerable();
var requestInfo = this.Request<IHasIEnumerableKeyValuePairsQueryMap>(x => x.FooAsync(queryMap));

var queryParams = requestInfo.QueryParams.ToList();

Assert.Single(queryParams);
var queryParam0 = queryParams[0].SerializeToString(null).ToList();
Assert.Single(queryParam0);
Assert.Equal(pair.Key, queryParam0[0].Key);
Assert.Equal(pair.Value, queryParam0[0].Value);
}

[Fact]
public void HandlesIEnumerableGenericKeyValuePairs()
{
var pair = new KeyValuePair<string, List<string>>("k1", new List<string>{ "v1" });
var queryMap = new[]{ pair }.AsEnumerable();
var requestInfo = this.Request<IHasIEnumerableGenericKeyValuePairsQueryMap>(x => x.FooAsync(queryMap));

var queryParams = requestInfo.QueryParams.ToList();

Assert.Single(queryParams);
var queryParam0 = queryParams[0].SerializeToString(null).ToList();
Assert.Single(queryParam0);
Assert.Equal(pair.Key, queryParam0[0].Key);
Assert.Equal(pair.Value[0], queryParam0[0].Value);
}

[Fact]
public void HandlesGenericIEnumerableKeyValuePairs()
{
var pair = new KeyValuePair<string, string>("k1", "v1");
var queryMap = new[]{ pair }.AsEnumerable();
var requestInfo = this.Request<IHasGenericIEnumerableKeyValuePairsQueryMap>(x => x.FooAsync(queryMap));

var queryParams = requestInfo.QueryParams.ToList();

Assert.Single(queryParams);
var queryParam0 = queryParams[0].SerializeToString(null).ToList();
Assert.Single(queryParam0);
Assert.Equal(pair.Key, queryParam0[0].Key);
Assert.Equal(pair.Value, queryParam0[0].Value);
}

[Fact]
public void HandlesGenericIEnumerableGenericKeyValuePairs()
{
var pair = new KeyValuePair<string, List<string>>("k1", new List<string>{ "v1" });
var queryMap = new[]{ pair }.AsEnumerable();
var requestInfo = this.Request<IHasGenericIEnumerableGenericKeyValuePairsQueryMap>(x =>
x.FooAsync<IEnumerable<KeyValuePair<string, List<string>>>, string, List<string>>(queryMap));

var queryParams = requestInfo.QueryParams.ToList();

Assert.Single(queryParams);
var queryParam0 = queryParams[0].SerializeToString(null).ToList();
Assert.Single(queryParam0);
Assert.Equal(pair.Key, queryParam0[0].Key);
Assert.Equal(pair.Value[0], queryParam0[0].Value);
}

[Fact]
public void HandlesGenericIEnumerableGenericKeyGenericListValuePairs()
{
var pair = new KeyValuePair<string, List<string>>("k1", new List<string>{ "v1" });
var queryMap = new[]{ pair }.AsEnumerable();
var requestInfo = this.Request<IHasGenericIEnumerableGenericKeyGenericListValueQueryMap>(x =>
x.FooAsync<IEnumerable<KeyValuePair<string, List<string>>>, string, List<string>, string>(queryMap));

var queryParams = requestInfo.QueryParams.ToList();

Assert.Single(queryParams);
var queryParam0 = queryParams[0].SerializeToString(null).ToList();
Assert.Single(queryParam0);
Assert.Equal(pair.Key, queryParam0[0].Key);
Assert.Equal(pair.Value[0], queryParam0[0].Value);
}
}
}
5 changes: 3 additions & 2 deletions src/RestEase/Implementation/RequestInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ public void AddQueryCollectionParameter<T>(QuerySerializationMethod serializatio
/// <typeparam name="TValue">Type of value in the query map</typeparam>
/// <param name="serializationMethod">Method to use to serialize the value</param>
/// <param name="queryMap">Query map to add</param>
public void AddQueryMap<TKey, TValue>(QuerySerializationMethod serializationMethod, IDictionary<TKey, TValue> queryMap)
public void AddQueryMap<TKey, TValue>(QuerySerializationMethod serializationMethod, IEnumerable<KeyValuePair<TKey, TValue>> queryMap)
{
if (queryMap == null)
return;
Expand Down Expand Up @@ -223,7 +223,8 @@ kvp.Value is IEnumerable<object> enumerable &&
/// <typeparam name="TElement">Type of element in the value</typeparam>
/// <param name="serializationMethod">Method to use to serialize the value</param>
/// <param name="queryMap">Query map to add</param>
public void AddQueryCollectionMap<TKey, TValue, TElement>(QuerySerializationMethod serializationMethod, IDictionary<TKey, TValue> queryMap) where TValue : IEnumerable<TElement>
public void AddQueryCollectionMap<TKey, TValue, TElement>(QuerySerializationMethod serializationMethod, IEnumerable<KeyValuePair<TKey, TValue>> queryMap)
where TValue : IEnumerable<TElement>
{
if (queryMap == null)
return;
Expand Down