Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom enum description attribute property names #277

Merged
Merged
Show file tree
Hide file tree
Changes from 5 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
12 changes: 8 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -163,10 +163,10 @@ Calling `ToString` directly on enum members usually results in less than ideal o
```C#
public enum EnumUnderTest
{
[Description("Custom description")]
MemberWithDescriptionAttribute,
MemberWithoutDescriptionAttribute,
ALLCAPITALS
[Description("Custom description")]
MemberWithDescriptionAttribute,
MemberWithoutDescriptionAttribute,
ALLCAPITALS
}
```

Expand All @@ -186,6 +186,10 @@ EnumUnderTest.MemberWithoutDescriptionAttribute.Humanize().Transform(To.TitleCas
You are not limited to `DescriptionAttribute` for custom description. Any attribute applied on enum members with a `string Description` property counts.
This is to help with platforms with missing `DescriptionAttribute` and also for allowing subclasses of the `DescriptionAttribute`.

You can even configure the name of the property of attibute to use as description.

`Configurator.EnumDescriptionPropertyNames[typeof(EnumUnderTest)] = "Info"`

Hopefully this will help avoid littering enums with unnecessary attributes!

###<a id="dehumanize-enums">Dehumanize Enums</a>
Expand Down
1 change: 1 addition & 0 deletions release_notes.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
###In Development
- [#257](https://github.com/MehdiK/Humanizer/pull/277): Added support for custom enum description attribute property names

[Commits](https://github.com/MehdiK/Humanizer/compare/v1.26.1...master)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ public class Configurator
{
public Humanizer.Configuration.LocaliserRegistry<Humanizer.Localisation.CollectionFormatters.ICollectionFormatter> CollectionFormatters { get; }
public Humanizer.DateTimeHumanizeStrategy.IDateTimeHumanizeStrategy DateTimeHumanizeStrategy { get; set; }
public System.Collections.Generic.IDictionary<System.Type, string> EnumDescriptionPropertyNames { get; }
public Humanizer.Configuration.LocaliserRegistry<Humanizer.Localisation.Formatters.IFormatter> Formatters { get; }
public Humanizer.Configuration.LocaliserRegistry<Humanizer.Localisation.NumberToWords.INumberToWordsConverter> NumberToWordsConverters { get; }
public Humanizer.Configuration.LocaliserRegistry<Humanizer.Localisation.Ordinalizers.IOrdinalizer> Ordinalizers { get; }
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
using System;
using Humanizer.Configuration;
using Xunit;

namespace Humanizer.Tests
{
public class EnumHumanizeWithCustomDescriptionPropertyNamesTests : IDisposable
{
public EnumHumanizeWithCustomDescriptionPropertyNamesTests()
{
Configurator.EnumDescriptionPropertyNames[typeof (EnumUnderTest)] = "Info";
}

public void Dispose()
{
Configurator.EnumDescriptionPropertyNames.Remove(typeof (EnumUnderTest));
}

[Fact]
public void HonorsCustomPropertyAttribute()
{
Assert.Equal(EnumTestsResources.MemberWithCustomPropertyAttribute, EnumUnderTest.MemberWithCustomPropertyAttribute.Humanize());
}

[Fact]
public void CanHumanizeMembersWithoutDescriptionAttribute()
{
Assert.Equal(EnumTestsResources.MemberWithoutDescriptionAttributeSentence, EnumUnderTest.MemberWithoutDescriptionAttribute.Humanize());
}
}
}
13 changes: 13 additions & 0 deletions src/Humanizer.Tests/EnumUnderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ public enum EnumUnderTest
MemberWithCustomDescriptionAttribute,
[ImposterDescription(42)]
MemberWithImposterDescriptionAttribute,
[CustomProperty(EnumTestsResources.MemberWithCustomPropertyAttribute)]
MemberWithCustomPropertyAttribute,
MemberWithoutDescriptionAttribute,
ALLCAPITALS
}
Expand All @@ -23,6 +25,7 @@ public class EnumTestsResources
public const string MemberWithDescriptionAttributeSubclass = "Description in Description subclass";
public const string MemberWithCustomDescriptionAttribute = "Description in custom Description attribute";
public const string MemberWithImposterDescriptionAttribute = "Member with imposter description attribute";
public const string MemberWithCustomPropertyAttribute = "Description in custom property attribute";
public const string MemberWithoutDescriptionAttributeSentence = "Member without description attribute";
public const string MemberWithoutDescriptionAttributeTitle = "Member Without Description Attribute";
public const string MemberWithoutDescriptionAttributeLowerCase = "member without description attribute";
Expand Down Expand Up @@ -59,4 +62,14 @@ public override string Description
get { return "Overridden " + base.Description; }
}
}

public class CustomPropertyAttribute : Attribute
{
public string Info { get; set; }

public CustomPropertyAttribute(string info)
{
Info = info;
}
}
}
1 change: 1 addition & 0 deletions src/Humanizer.Tests/Humanizer.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
<Compile Include="CasingTests.cs" />
<Compile Include="DateHumanize.cs" />
<Compile Include="CollectionHumanizeTests.cs" />
<Compile Include="EnumHumanizeWithCustomDescriptionPropertyNamesTests.cs" />
<Compile Include="Localisation\bg\DateHumanizeTests.cs" />
<Compile Include="Localisation\bg\TimeSpanHumanizeTests.cs" />
<Compile Include="Localisation\cs\DateHumanizeTests.cs" />
Expand Down
17 changes: 17 additions & 0 deletions src/Humanizer/Configuration/Configurator.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using System;
using System.Collections.Generic;
using Humanizer.DateTimeHumanizeStrategy;
using Humanizer.Localisation.Formatters;
using Humanizer.Localisation.NumberToWords;
Expand Down Expand Up @@ -98,5 +100,20 @@ public static IDateTimeHumanizeStrategy DateTimeHumanizeStrategy
get { return _dateTimeHumanizeStrategy; }
set { _dateTimeHumanizeStrategy = value; }
}

private const string DefaultEnumDescriptionPropertyName = "Description";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure about the implementation. It feels a bit complex. What if we just created a DescriptionPropertyLocator predicate function? Let them keep the logic. This way they can be as flexible as they want!

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wanted to provide the possibility to have different property names per enum type.
But as already mentioned it a first attempt, so let discuss further the possibilities.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After a second thought, it would be flexible enough since the PropertyInfo has the reference to the DeclaringType, so even my use case can be implemented with the predicate function.

I'll rewrite it now.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if that level of granular control is necessary to be honest with you. Also remember PropertyInfo is from the attribute not the target enum!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe we can just expose Func<PropertyInfo, bool>? That way they won't be able to nominate different attributes for different enums but I call YAGNI on it for now.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're right. Func<PropertyInfo, bool> is done.

private static readonly Dictionary<Type, string> _enumDescriptionPropertyNames = new Dictionary<Type, string>();
internal static string EnumDescriptionPropertyNameFor(Type type)
{
string result = _enumDescriptionPropertyNames.TryGetValue(type, out result) ? result : null;
return result ?? DefaultEnumDescriptionPropertyName;
}
/// <summary>
/// The registry of custom attribute property names for Enum.Humanize
/// </summary>
public static IDictionary<Type, string> EnumDescriptionPropertyNames
{
get { return _enumDescriptionPropertyNames; }
}
}
}
18 changes: 12 additions & 6 deletions src/Humanizer/EnumHumanizeExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Reflection;
using Humanizer.Configuration;

namespace Humanizer
{
Expand All @@ -9,7 +10,7 @@ namespace Humanizer
/// </summary>
public static class EnumHumanizeExtensions
{
private static readonly Func<PropertyInfo, bool> DescriptionProperty = p => p.Name == "Description" && p.PropertyType == typeof (string);
private static readonly Func<PropertyInfo, bool> StringTypedProperty = p => p.PropertyType == typeof(string);

/// <summary>
/// Turns an enum member into a human readable string; e.g. AnonymousUser -> Anonymous user. It also honors DescriptionAttribute data annotation
Expand All @@ -19,28 +20,33 @@ public static class EnumHumanizeExtensions
public static string Humanize(this Enum input)
{
Type type = input.GetType();
var memInfo = type.GetMember(input.ToString());
var caseName = input.ToString();
var memInfo = type.GetMember(caseName);

if (memInfo.Length > 0)
{
var customDescription = GetCustomDescription(memInfo[0]);
var propertyName = Configurator.EnumDescriptionPropertyNameFor(type);
var customDescription = GetCustomDescription(memInfo[0], propertyName);

if (customDescription != null)
return customDescription;
}

return input.ToString().Humanize();
return caseName.Humanize();
}

// I had to add this method because PCL doesn't have DescriptionAttribute & I didn't want two versions of the code & thus the reflection
private static string GetCustomDescription(MemberInfo memberInfo)
private static string GetCustomDescription(MemberInfo memberInfo, string propertyName)
{
var attrs = memberInfo.GetCustomAttributes(true);

foreach (var attr in attrs)
{
var attrType = attr.GetType();
var descriptionProperty = attrType.GetProperties().FirstOrDefault(DescriptionProperty);
var descriptionProperty =
attrType.GetProperties()
.Where(StringTypedProperty)
.FirstOrDefault(p => p.Name == propertyName);
if (descriptionProperty != null)
return descriptionProperty.GetValue(attr, null).ToString();
}
Expand Down