Skip to content

Commit

Permalink
Merge pull request #44 from aspriddell/query-collections
Browse files Browse the repository at this point in the history
Implement collection support for forms and queries
  • Loading branch information
aspriddell authored Nov 30, 2020
2 parents eb72717 + bd26760 commit be90a21
Show file tree
Hide file tree
Showing 8 changed files with 206 additions and 19 deletions.
49 changes: 49 additions & 0 deletions DragonFruit.Common.Data.Tests/QueryCompilationTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
// DragonFruit.Common Copyright 2020 DragonFruit Network
// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details

using System.Linq;
using DragonFruit.Common.Data.Parameters;
using NUnit.Framework;

namespace DragonFruit.Common.Data.Tests
{
[TestFixture]
public class QueryCompilationTests
{
[TestCase]
public void TestQueries()
{
var query = new TestRequest().FullUrl.Split('?').Last().Split('&');

for (int i = 0; i < TestRequest.TestDataset.Length; i++)
{
var testString = TestRequest.TestDataset[i];
Assert.IsTrue(query.Contains($"{TestRequest.QueryName}={testString}"));
Assert.IsTrue(query.Contains($"{TestRequest.QueryName}[]={testString}"));
Assert.IsTrue(query.Contains($"{TestRequest.QueryName}[{i}]={testString}"));
}

Assert.IsTrue(query.Contains($"{TestRequest.QueryName}={string.Join(":", TestRequest.TestDataset)}"));
}
}

internal class TestRequest : ApiRequest
{
internal const string QueryName = "data";
internal static readonly string[] TestDataset = { "a", "b", "c" };

public override string Path => "http://example.com";

[QueryParameter(QueryName, CollectionConversionMode.Recursive)]
public string[] RecursiveData { get; set; } = TestDataset;

[QueryParameter(QueryName, CollectionConversionMode.Ordered)]
public string[] OrderedData { get; set; } = TestDataset;

[QueryParameter(QueryName, CollectionConversionMode.Unordered)]
public string[] UnorderedData { get; set; } = TestDataset;

[QueryParameter(QueryName, CollectionConversionMode.Concatenated, CollectionSeparator = ":")]
public string[] ConcatenatedData { get; set; } = TestDataset;
}
}
1 change: 0 additions & 1 deletion DragonFruit.Common.Data/ApiRequest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System.Globalization;
using System.Net.Http;
using System.Net.Http.Headers;
using DragonFruit.Common.Data.Exceptions;
using DragonFruit.Common.Data.Parameters;
using DragonFruit.Common.Data.Serializers;
using DragonFruit.Common.Data.Utils;
Expand Down
23 changes: 23 additions & 0 deletions DragonFruit.Common.Data/Methods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,27 @@ public enum BodyType
/// </summary>
Custom
}

public enum CollectionConversionMode
{
/// <summary>
/// The query name is repeated and a new element created for each (a=1&a=2&a=3)
/// </summary>
Recursive,

/// <summary>
/// The query name has indexer symbols appended with no order (a[]=1&a[]=2&a[]=3)
/// </summary>
Unordered,

/// <summary>
/// The query name has indexer symbols appended explicit order inserted (a[0]=1&a[1]=2&a[2]=3)
/// </summary>
Ordered,

/// <summary>
/// The query is concatenated with a string and merged with one key (a=1,2,3)
/// </summary>
Concatenated
}
}
17 changes: 16 additions & 1 deletion DragonFruit.Common.Data/Parameters/FormParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,31 @@

using System;

#nullable enable

namespace DragonFruit.Common.Data.Parameters
{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
public class FormParameter : Attribute, IProperty
{
public FormParameter()
{
}

public FormParameter(string name)
{
Name = name;
}

public string Name { get; }
public FormParameter(string name, CollectionConversionMode collectionHandling)
: this(name)
{
CollectionHandling = collectionHandling;
}

public string? Name { get; set; }
public CollectionConversionMode? CollectionHandling { get; set; }

public string? CollectionSeparator { get; set; }
}
}
8 changes: 7 additions & 1 deletion DragonFruit.Common.Data/Parameters/IProperty.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
// DragonFruit.Common Copyright 2020 DragonFruit Network
// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details

#nullable enable

namespace DragonFruit.Common.Data.Parameters
{
public interface IProperty
{
string Name { get; }
string? Name { get; set; }

CollectionConversionMode? CollectionHandling { get; set; }

string? CollectionSeparator { get; set; }
}
}
16 changes: 15 additions & 1 deletion DragonFruit.Common.Data/Parameters/QueryParameter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,30 @@

using System;

#nullable enable

namespace DragonFruit.Common.Data.Parameters
{
[AttributeUsage(AttributeTargets.Property | AttributeTargets.Parameter)]
public class QueryParameter : Attribute, IProperty
{
public QueryParameter()
{
}

public QueryParameter(string name)
{
Name = name;
}

public string Name { get; }
public QueryParameter(string name, CollectionConversionMode collectionConversionMode)
: this(name)
{
CollectionHandling = collectionConversionMode;
}

public string? Name { get; set; }
public CollectionConversionMode? CollectionHandling { get; set; }
public string? CollectionSeparator { get; set; }
}
}
8 changes: 8 additions & 0 deletions DragonFruit.Common.Data/Utils/CultureUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,13 @@ public static CultureInfo DefaultCulture
get => _defaultCulture ?? CultureInfo.InvariantCulture;
set => _defaultCulture = value;
}

internal static string AsString(this object value, CultureInfo culture = null) => value switch
{
bool boolVar => boolVar.ToString().ToLower(culture ?? DefaultCulture),
null => null,

_ => value.ToString()
};
}
}
103 changes: 88 additions & 15 deletions DragonFruit.Common.Data/Utils/ParameterUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License. Please refer to the LICENSE file at the root of this project for details

using System;
using System.Collections;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
Expand All @@ -12,6 +13,8 @@ namespace DragonFruit.Common.Data.Utils
{
public static class ParameterUtils
{
private const string DefaultConcatenationCharacter = ",";

/// <summary>
/// Default <see cref="BindingFlags"/> to search for matching properties
/// </summary>
Expand All @@ -22,26 +25,45 @@ public static class ParameterUtils
/// </summary>
internal static IEnumerable<KeyValuePair<string, string>> GetParameter<T>(object host, CultureInfo culture) where T : IProperty
{
var type = typeof(T);

foreach (var property in host.GetType().GetProperties(DefaultFlags))
{
if (!(Attribute.GetCustomAttribute(property, type) is T parameter))
if (!property.CanRead || !(Attribute.GetCustomAttribute(property, typeof(T)) is T attribute))
{
continue;
}

var keyName = attribute.Name ?? property.Name;
var propertyValue = property.GetValue(host);

if (propertyValue == null)
{
// ignore null values
continue;
}

var value = property.GetValue(host, null);
string convertedValue = value switch
// check if the type we've got is an IEnumerable of anything AND we have a valid collection handler mode
if (attribute.CollectionHandling.HasValue && typeof(IEnumerable).IsAssignableFrom(property.PropertyType))
{
bool boolVar => boolVar.ToString().ToLower(culture),
null => null,
Func<IEnumerable<object>, string, CultureInfo, IEnumerable<KeyValuePair<string, string>>> entityConverter = attribute.CollectionHandling switch
{
CollectionConversionMode.Recursive => ApplyRecursiveConversion,
CollectionConversionMode.Unordered => ApplyUnorderedConversion,
CollectionConversionMode.Ordered => ApplyOrderedConversion,
CollectionConversionMode.Concatenated => (a, b, c) => ApplyConcatenation(a, b, c, attribute.CollectionSeparator ?? DefaultConcatenationCharacter),

_ => value.ToString()
};
_ => throw new ArgumentOutOfRangeException()
};

if (convertedValue != null)
yield return new KeyValuePair<string, string>(parameter.Name, convertedValue);
foreach (var entry in entityConverter.Invoke((IEnumerable<object>)propertyValue, keyName, culture))
{
// we purposely keep nulls in here, as it might affect the ordering.
yield return entry;
}
}
else
{
yield return propertyValue.ToKeyValuePair(keyName, culture);
}
}
}

Expand All @@ -50,10 +72,61 @@ internal static IEnumerable<KeyValuePair<string, string>> GetParameter<T>(object
/// </summary>
internal static object GetSingleParameterObject<T>(object host) where T : Attribute
{
return host.GetType()
.GetProperties(DefaultFlags)
.Single(x => Attribute.GetCustomAttribute(x, typeof(T)) is T)
.GetValue(host, null);
var targetType = typeof(T);
var attributedProperty = host.GetType()
.GetProperties(DefaultFlags)
.SingleOrDefault(x => Attribute.GetCustomAttribute(x, targetType) is T);

if (attributedProperty == default)
{
throw new KeyNotFoundException($"No valid {targetType.Name} was attributed. There must be a single attributed property");
}

if (!attributedProperty.CanRead)
{
throw new MemberAccessException($"Unable to read contents of property {attributedProperty.Name}");
}

return attributedProperty.GetValue(host);
}

#region IEnumerable Converters

private static IEnumerable<KeyValuePair<string, string>> ApplyRecursiveConversion(IEnumerable<object> values, string keyName, CultureInfo culture)
{
return values.Select(x => x.ToKeyValuePair(keyName, culture));
}

private static IEnumerable<KeyValuePair<string, string>> ApplyUnorderedConversion(IEnumerable<object> values, string keyName, CultureInfo culture)
{
return values.Select(x => x.ToKeyValuePair($"{keyName}[]", culture));
}

private static IEnumerable<KeyValuePair<string, string>> ApplyOrderedConversion(IEnumerable<object> values, string keyName, CultureInfo culture)
{
var counter = 0;
var enumerator = values.GetEnumerator();

while (enumerator.MoveNext())
{
yield return enumerator.Current.ToKeyValuePair($"{keyName}[{counter}]", culture);

counter++;
}

enumerator.Dispose();
}

private static IEnumerable<KeyValuePair<string, string>> ApplyConcatenation(IEnumerable<object> values, string keyName, CultureInfo culture, string concatCharacter)
{
yield return new KeyValuePair<string, string>(keyName, string.Join(concatCharacter, values.Select(x => x.AsString(culture))));
}

private static KeyValuePair<string, string> ToKeyValuePair(this object value, string key, CultureInfo culture)
{
return new KeyValuePair<string, string>(key, value.AsString(culture));
}

#endregion
}
}

0 comments on commit be90a21

Please sign in to comment.