diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml index acb4ed3..17414ab 100644 --- a/.github/workflows/dotnet.yml +++ b/.github/workflows/dotnet.yml @@ -8,7 +8,7 @@ on: jobs: build-ubuntu: - runs-on: ubuntu-latest + runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 diff --git a/src/Searchlight/Parsing/SyntaxParser.cs b/src/Searchlight/Parsing/SyntaxParser.cs index cc1a8c3..9241514 100644 --- a/src/Searchlight/Parsing/SyntaxParser.cs +++ b/src/Searchlight/Parsing/SyntaxParser.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Reflection; +using System.Runtime.Serialization; using Searchlight.Exceptions; using Searchlight.Expressions; using Searchlight.Nesting; @@ -592,17 +594,43 @@ private static IExpressionValue ParseParameter(DataSource source, SyntaxTree syn // Special case for enum types if (column.EnumType != null || fieldType.IsEnum) { + var fields = fieldType.GetFields(BindingFlags.Public | BindingFlags.Static); + var enumMembers = fields.Select(f => + { + var attr = f.GetCustomAttribute(); + if (attr != null && attr.Value != null) + { + return (f.Name, attr.Value); + } + return (null, null); + }).Where(e => e.Name != null).ToList(); + + var dictionary = enumMembers.ToDictionary(p => p.Value, p => p.Name); + try { + if (dictionary.TryGetValue(valueToken, out var enumValueToken)) + { + valueToken = enumValueToken; + } var parsed = Enum.Parse(column.EnumType ?? fieldType, valueToken); return ConstantValue.From(Convert.ChangeType(parsed, fieldType)); } catch { + var expectedTokens = Enum.GetNames(column.EnumType ?? fieldType); + var valueDictionary = enumMembers.ToDictionary(p => p.Name, p => p.Value); + for (var i = 0; i < expectedTokens.Length; i++) + { + if (valueDictionary.TryGetValue(expectedTokens[i], out var newToken)) + { + expectedTokens[i] = newToken; + } + } syntax.AddError(new InvalidToken { BadToken = valueToken, - ExpectedTokens = Enum.GetNames(column.EnumType ?? fieldType), + ExpectedTokens = expectedTokens, OriginalFilter = tokens.OriginalText, }); return null; diff --git a/tests/Searchlight.Tests/ParseModelTests.cs b/tests/Searchlight.Tests/ParseModelTests.cs index f26a341..dd1431b 100644 --- a/tests/Searchlight.Tests/ParseModelTests.cs +++ b/tests/Searchlight.Tests/ParseModelTests.cs @@ -2,6 +2,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using Searchlight.Query; using System.Linq; +using System.Runtime.Serialization; using System.Threading.Tasks; using Searchlight.Exceptions; using Searchlight.Expressions; @@ -509,6 +510,14 @@ public enum TestEnumValueCategory Generic = 2, } + public enum TestEnumAttributeValueCategory + { + [EnumMember(Value = "Not Anything")] + None = 0, + Special = 1, + Generic = 2, + } + [SearchlightModel(DefaultSort = "Name ascending")] public class TestClassWithEnumValues { @@ -516,6 +525,8 @@ public class TestClassWithEnumValues public string Name { get; set; } [SearchlightField(OriginalName = "field_category")] public TestEnumValueCategory Category { get; set; } + [SearchlightField(OriginalName = "field_category_attribute")] + public TestEnumAttributeValueCategory AttributeCategory { get; set; } } @@ -524,11 +535,13 @@ public void TestValidEnumFilters() { var source = DataSource.Create(null, typeof(TestClassWithEnumValues), AttributeMode.Strict); var columns = source.GetColumnDefinitions().ToArray(); - Assert.AreEqual(2, columns.Length); + Assert.AreEqual(3, columns.Length); Assert.AreEqual("Name", columns[0].FieldName); Assert.AreEqual(typeof(string), columns[0].FieldType); Assert.AreEqual("Category", columns[1].FieldName); Assert.AreEqual(typeof(TestEnumValueCategory), columns[1].FieldType); + Assert.AreEqual("AttributeCategory", columns[2].FieldName); + Assert.AreEqual(typeof(TestEnumAttributeValueCategory), columns[2].FieldType); // Query for a valid category var syntax1 = source.ParseFilter("category = None"); @@ -537,12 +550,26 @@ public void TestValidEnumFilters() // Query using the raw integer value, which is generally not advised but we accept it for historical reasons var syntax2 = source.ParseFilter("category = 0"); Assert.IsNotNull(syntax2); + + // Query attribute value + var syntax3 = source.ParseFilter("attributecategory = 'Not Anything'"); + Assert.IsNotNull(syntax3); + + // Query non attribute value in mixed enum + var syntax4 = source.ParseFilter("attributecategory = Special"); + Assert.IsNotNull(syntax4); // Query for a non-valid category var ex2 = Assert.ThrowsException(() => source.ParseFilter("category = InvalidValue")); Assert.AreEqual("InvalidValue", ex2.BadToken); CollectionAssert.AreEqual(new string[] { "None", "Special", "Generic" }, ex2.ExpectedTokens); Assert.AreEqual("The filter statement contained an unexpected token, 'InvalidValue'. Searchlight expects to find one of these next: None, Special, Generic", ex2.ErrorMessage); + + // Query for a non-valid category in attribute enum + var ex3 = Assert.ThrowsException(() => source.ParseFilter("attributecategory = InvalidValue")); + Assert.AreEqual("InvalidValue", ex3.BadToken); + CollectionAssert.AreEqual(new string[] { "Not Anything", "Special", "Generic" }, ex3.ExpectedTokens); + Assert.AreEqual("The filter statement contained an unexpected token, 'InvalidValue'. Searchlight expects to find one of these next: Not Anything, Special, Generic", ex3.ErrorMessage); } [SearchlightModel(DefaultSort = nameof(Name))] diff --git a/tests/Searchlight.Tests/Searchlight.Tests.csproj b/tests/Searchlight.Tests/Searchlight.Tests.csproj index a4dc112..cd0e7a3 100644 --- a/tests/Searchlight.Tests/Searchlight.Tests.csproj +++ b/tests/Searchlight.Tests/Searchlight.Tests.csproj @@ -21,10 +21,10 @@ - - - - + + + +