From 5cee6501dbbec9d1310902fb15785a2b70abfe27 Mon Sep 17 00:00:00 2001 From: Haacked Date: Sun, 27 Oct 2013 18:10:45 -0700 Subject: [PATCH] Add base class for request parameter classes --- .../Models/RequestParametersTests.cs | 165 ++++++++++++++++++ Octokit.Tests/Octokit.Tests.csproj | 1 + Octokit.Tests/OctokitRT.Tests.csproj | 1 + Octokit/Helpers/ParameterAttribute.cs | 11 ++ Octokit/Helpers/ReflectionExtensions.cs | 17 ++ Octokit/Models/Request/RequestParameters.cs | 88 ++++++++++ Octokit/Octokit.csproj | 3 + Octokit/OctokitRT.csproj | 1 + 8 files changed, 287 insertions(+) create mode 100644 Octokit.Tests/Models/RequestParametersTests.cs create mode 100644 Octokit/Helpers/ParameterAttribute.cs create mode 100644 Octokit/Helpers/ReflectionExtensions.cs create mode 100644 Octokit/Models/Request/RequestParameters.cs diff --git a/Octokit.Tests/Models/RequestParametersTests.cs b/Octokit.Tests/Models/RequestParametersTests.cs new file mode 100644 index 00000000..516e2369 --- /dev/null +++ b/Octokit.Tests/Models/RequestParametersTests.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using Octokit.Internal; +using Xunit; + +namespace Octokit.Tests.Models +{ + public class RequestParametersTests + { + public class TheToParametersDictionaryMethod + { + [Fact] + public void CanConvertObjectToLowercaseDictionary() + { + var model = new SimpleRequestParameters { Bar = 123, Foo = "foovalue" }; + + var result = model.ToParametersDictionary(); + + Assert.Equal(2, result.Count); + Assert.Equal("123", result["bar"]); + Assert.Equal("foovalue", result["foo"]); + } + + [Fact] + public void OmitsNullValues() + { + var model = new SimpleRequestParameters { Bar = 123 }; + + var result = model.ToParametersDictionary(); + + Assert.Equal(1, result.Count); + Assert.DoesNotContain("foo", result.Keys); + Assert.Equal("123", result["bar"]); + } + + [Fact] + public void HandlesEnums() + { + var model = new ClassWithEnum { Nom = Enomnomnom.Yuck }; + + var result = model.ToParametersDictionary(); + + Assert.Equal(1, result.Count); + Assert.Equal("yuck", result["nom"]); + } + + [Fact] + public void TreatsFirstEnumMemberAsDefault() + { + var model = new ClassWithEnum(); + + var result = model.ToParametersDictionary(); + + Assert.Equal(1, result.Count); + Assert.Equal("bland", result["nom"]); + } + + [Fact] + public void HandlesEnumsWithSpecifiedValue() + { + var model = new ClassWithEnum { Nom = Enomnomnom.NomNomNom }; + + var result = model.ToParametersDictionary(); + + Assert.Equal(1, result.Count); + Assert.Equal("noms", result["nom"]); + } + + [Fact] + public void ConvertsDatetTimeOffsetToIso8601Format() + { + var model = new WithDateTime + { + When = DateTimeOffset.ParseExact("Wed 23 Jan 2013 8:30 AM -08:00", + "ddd dd MMM yyyy h:mm tt zzz", CultureInfo.InvariantCulture) + }; + + var result = model.ToParametersDictionary(); + + Assert.Equal(1, result.Count); + Assert.Equal("2013-01-23T16:30:00Z", result["when"]); + } + + [Fact] + public void OmitsNullDatetTimeOffsetToIso8601Format() + { + var model = new WithDateTime(); + + var result = model.ToParametersDictionary(); + + Assert.Equal(0, result.Count); + } + + [Fact] + public void JoinsStringCollectionIntoCommaSeparatedString() + { + var model = new ClassWithStringCollection { Strings = new List { "one", "two" } }; + + var result = model.ToParametersDictionary(); + + Assert.Equal(1, result.Count); + Assert.Equal("one,two", result["strings"]); + } + + [Fact] + public void DoesNotIncludeEmptyList() + { + var model = new ClassWithStringCollection { Strings = new List() }; + + var result = model.ToParametersDictionary(); + + Assert.Equal(0, result.Count); + } + + [Fact] + public void UsesParameterAttributeForKey() + { + var model = new WithPropertyNameDifferentFromKey() { LongPropertyName = "verbose" }; + + var result = model.ToParametersDictionary(); + + Assert.Equal(1, result.Count); + Assert.Equal("verbose", result["prop"]); + } + + public class SimpleRequestParameters : RequestParameters + { + public string Foo { get; set; } + public int Bar { get; set; } + } + + public class ClassWithEnum : RequestParameters + { + public Enomnomnom Nom { get; set; } + } + + public class WithDateTime : RequestParameters + { + public DateTimeOffset? When { get; set; } + } + + public class ClassWithStringCollection : RequestParameters + { + public ICollection Strings { get; set; } + } + + public class WithPropertyNameDifferentFromKey : RequestParameters + { + [Parameter(Key = "prop")] + public string LongPropertyName { get; set; } + } + + public enum Enomnomnom + { + Bland, + Delicious, + Yuck, + + [Parameter(Value = "noms")] + NomNomNom + } + } + } +} diff --git a/Octokit.Tests/Octokit.Tests.csproj b/Octokit.Tests/Octokit.Tests.csproj index 8bee7a41..9301165c 100644 --- a/Octokit.Tests/Octokit.Tests.csproj +++ b/Octokit.Tests/Octokit.Tests.csproj @@ -98,6 +98,7 @@ + diff --git a/Octokit.Tests/OctokitRT.Tests.csproj b/Octokit.Tests/OctokitRT.Tests.csproj index 70f33577..17ea6ba9 100644 --- a/Octokit.Tests/OctokitRT.Tests.csproj +++ b/Octokit.Tests/OctokitRT.Tests.csproj @@ -89,6 +89,7 @@ + diff --git a/Octokit/Helpers/ParameterAttribute.cs b/Octokit/Helpers/ParameterAttribute.cs new file mode 100644 index 00000000..224c606d --- /dev/null +++ b/Octokit/Helpers/ParameterAttribute.cs @@ -0,0 +1,11 @@ +using System; + +namespace Octokit.Internal +{ + [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)] + public sealed class ParameterAttribute : Attribute + { + public string Key { get; set; } + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/Octokit/Helpers/ReflectionExtensions.cs b/Octokit/Helpers/ReflectionExtensions.cs new file mode 100644 index 00000000..d7966b9b --- /dev/null +++ b/Octokit/Helpers/ReflectionExtensions.cs @@ -0,0 +1,17 @@ +using System; + +namespace Octokit +{ + internal static class ReflectionExtensions + { + public static bool IsDateTimeOffset(this Type type) + { + return type == typeof(DateTimeOffset) || type == typeof(DateTimeOffset?); + } + + public static bool IsNullable(this Type type) + { + return type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>); + } + } +} diff --git a/Octokit/Models/Request/RequestParameters.cs b/Octokit/Models/Request/RequestParameters.cs new file mode 100644 index 00000000..808512d6 --- /dev/null +++ b/Octokit/Models/Request/RequestParameters.cs @@ -0,0 +1,88 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Linq; +using System.Reflection; +using Octokit.Internal; + +namespace Octokit +{ + /// + /// Base class for classes which represent query string parameters to certain API endpoints. + /// + public abstract class RequestParameters + { + static readonly ConcurrentDictionary> _propertiesMap = + new ConcurrentDictionary>(); + + public virtual IDictionary ToParametersDictionary() + { + var properties = _propertiesMap.GetOrAdd(GetType(), GetPropertiesForType); + + var dict = (from property in properties + let value = GetValue(property) + let key = GetKey(property) + where value != null + select new { key, value }).ToDictionary(kvp => kvp.key, kvp => kvp.value); + return dict; + } + + static List GetPropertiesForType(Type type) + { + return type.GetProperties(BindingFlags.Instance | BindingFlags.Public).ToList(); + } + + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", + Justification = "GitHub API depends on lower case strings")] + static string GetKey(PropertyInfo property) + { + var attribute = property.GetCustomAttributes(typeof(ParameterAttribute), false) + .Cast() + .FirstOrDefault(attr => attr.Key != null); + + return attribute == null + ? property.Name.ToLowerInvariant() + : attribute.Key; + } + + [SuppressMessage("Microsoft.Globalization", "CA1308:NormalizeStringsToUppercase", + Justification = "GitHub API depends on lower case strings")] + string GetValue(PropertyInfo property) + { + var value = property.GetValue(this, null); + + if (typeof(IEnumerable).IsAssignableFrom(property.PropertyType)) + { + var list = (IEnumerable)value; + return !list.Any() ? null : String.Join(",", list); + } + + if (property.PropertyType.IsDateTimeOffset() && value != null) + { + return ((DateTimeOffset)value).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ", + CultureInfo.InvariantCulture); + } + + if (property.PropertyType.IsEnum && value != null) + { + var member = property.PropertyType.GetMember(value.ToString()).FirstOrDefault(); + if (member != null) + { + var attribute = member.GetCustomAttributes(typeof(ParameterAttribute), false) + .Cast() + .FirstOrDefault(); + if (attribute != null) + { + return attribute.Value; + } + } + } + + return value != null + ? value.ToString().ToLowerInvariant() + : null; + } + } +} diff --git a/Octokit/Octokit.csproj b/Octokit/Octokit.csproj index 573bff4f..a7273532 100644 --- a/Octokit/Octokit.csproj +++ b/Octokit/Octokit.csproj @@ -88,8 +88,11 @@ + + + diff --git a/Octokit/OctokitRT.csproj b/Octokit/OctokitRT.csproj index 63c633cb..1ba1753c 100644 --- a/Octokit/OctokitRT.csproj +++ b/Octokit/OctokitRT.csproj @@ -148,6 +148,7 @@ +