diff --git a/Searchlight.nuspec b/Searchlight.nuspec index 684a992..17d86c4 100644 --- a/Searchlight.nuspec +++ b/Searchlight.nuspec @@ -2,7 +2,7 @@ Searchlight - 1.0.1 + 1.0.2 Searchlight Ted Spence Ted Spence @@ -17,12 +17,11 @@ A friendly and readable domain specific query language for your API. docs/icons8-searchlight-90.png - # 1.0.1 - July 30, 2023 + # 1.0.2 + September 10, 2023 - Added test containers for MySQL. - Revised some tests for clock drift. - Improved overall testing for clause string conversion. + * Added support for autocomplete. + * Fixed issues with handling of enum clauses in queries. Copyright 2013 - 2023 REST query language abstract syntax tree parser sql-injection protection friendly readable domain-specific DSL diff --git a/src/Searchlight/Autocomplete/CompletionList.cs b/src/Searchlight/Autocomplete/CompletionList.cs index 93460cd..0d0a1a8 100644 --- a/src/Searchlight/Autocomplete/CompletionList.cs +++ b/src/Searchlight/Autocomplete/CompletionList.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member namespace Searchlight.Autocomplete { diff --git a/src/Searchlight/DataSource.cs b/src/Searchlight/DataSource.cs index 7a6a4cd..86402a2 100644 --- a/src/Searchlight/DataSource.cs +++ b/src/Searchlight/DataSource.cs @@ -65,15 +65,15 @@ public class DataSource /// public DataSource WithColumn(string columnName, Type columnType) { - return WithRenamingColumn(columnName, columnName, null, columnType, null); + return WithRenamingColumn(columnName, columnName, null, columnType, null, null); } /// /// Add a column to this definition /// - public DataSource WithRenamingColumn(string filterName, string columnName, string[] aliases, Type columnType, string description) + public DataSource WithRenamingColumn(string filterName, string columnName, string[] aliases, Type columnType, Type enumType, string description) { - var columnInfo = new ColumnInfo(filterName, columnName, aliases, columnType, description); + var columnInfo = new ColumnInfo(filterName, columnName, aliases, columnType, enumType, description); _columns.Add(columnInfo); // Allow the API caller to either specify either the model name or one of the aliases @@ -200,9 +200,10 @@ public static DataSource Create(SearchlightEngine engine, Type modelType, Attrib if (filter != null) { // If this is a renaming column, add it appropriately - Type t = filter.FieldType ?? pi.PropertyType; - src.WithRenamingColumn(pi.Name, filter.OriginalName ?? pi.Name, - filter.Aliases ?? Array.Empty(), t, filter.Description); + var t = filter.FieldType ?? pi.PropertyType; + var columnName = filter.OriginalName ?? pi.Name; + var aliases = filter.Aliases ?? Array.Empty(); + src.WithRenamingColumn(pi.Name, columnName, aliases, t, filter.EnumType, filter.Description); } var collection = pi.GetCustomAttributes().FirstOrDefault(); diff --git a/src/Searchlight/Exceptions/TableNotFoundException.cs b/src/Searchlight/Exceptions/TableNotFoundException.cs index 01f9e33..c08424f 100644 --- a/src/Searchlight/Exceptions/TableNotFoundException.cs +++ b/src/Searchlight/Exceptions/TableNotFoundException.cs @@ -1,4 +1,6 @@ -namespace Searchlight.Exceptions +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Searchlight.Exceptions { public class TableNotFoundException : SearchlightException { diff --git a/src/Searchlight/LinqExecutor.cs b/src/Searchlight/LinqExecutor.cs index a179059..4e16b9d 100644 --- a/src/Searchlight/LinqExecutor.cs +++ b/src/Searchlight/LinqExecutor.cs @@ -163,6 +163,12 @@ private static Expression BuildOneExpression(ParameterExpression select, Base // Set up LINQ expressions for this object var valueType = criteria.Column.FieldType; field = Expression.Property(select, criteria.Column.FieldName); + if (field.Type.IsEnum) + { + valueType = field.Type; + rawValue = Enum.Parse(valueType, rawValue.ToString()); + } + value = Expression.Constant(rawValue, valueType); switch (criteria.Operation) { diff --git a/src/Searchlight/Parsing/ColumnInfo.cs b/src/Searchlight/Parsing/ColumnInfo.cs index d4b2730..34fad94 100644 --- a/src/Searchlight/Parsing/ColumnInfo.cs +++ b/src/Searchlight/Parsing/ColumnInfo.cs @@ -14,12 +14,20 @@ public class ColumnInfo /// The name of the column in the database /// If this field is known by other names, list them here /// The raw type of the column in the database - public ColumnInfo(string filterName, string columnName, string[] aliases, Type columnType, string description) + /// The type of the enum that the column is mapped to + /// A description of the column for autocomplete + public ColumnInfo(string filterName, string columnName, string[] aliases, Type columnType, Type enumType, string description) { FieldName = filterName; OriginalName = columnName; FieldType = columnType; Aliases = aliases; + if (enumType != null && !enumType.IsEnum) + { + throw new ArgumentException("Must specify an enum type", nameof(enumType)); + } + + EnumType = enumType; Description = description; } @@ -43,6 +51,12 @@ public ColumnInfo(string filterName, string columnName, string[] aliases, Type c /// public Type FieldType { get; private set; } + /// + /// When the user specifies a field to be an enum, the parameter must be mapped by this type + /// ex. The field type is int and enum is CarType { Sedan = 0, SUV = 1 } so "Sedan" should translate to 0 + /// + public Type EnumType { get; private set; } + /// /// Detailed field documentation for autocomplete, if provided. /// diff --git a/src/Searchlight/Parsing/SyntaxParser.cs b/src/Searchlight/Parsing/SyntaxParser.cs index ae5ee82..24e23ee 100644 --- a/src/Searchlight/Parsing/SyntaxParser.cs +++ b/src/Searchlight/Parsing/SyntaxParser.cs @@ -8,6 +8,9 @@ namespace Searchlight.Parsing { + /// + /// Static class for syntax parsing + /// public static class SyntaxParser { /// @@ -517,7 +520,27 @@ private static IExpressionValue ParseParameter(SyntaxTree syntax, ColumnInfo col return computedValue; } } - + + // Special case for enum types + if (column.EnumType != null || fieldType.IsEnum) + { + try + { + var parsed = Enum.Parse(column.EnumType ?? fieldType, valueToken); + return ConstantValue.From(Convert.ChangeType(parsed, fieldType)); + } + catch + { + syntax.AddError(new InvalidToken + { + BadToken = valueToken, + ExpectedTokens = Enum.GetNames(column.EnumType ?? fieldType), + OriginalFilter = tokens.OriginalText, + }); + return null; + } + } + // All other types use a basic type changer return ConstantValue.From(Convert.ChangeType(valueToken, fieldType)); } diff --git a/src/Searchlight/Parsing/Token.cs b/src/Searchlight/Parsing/Token.cs index cfcf86b..6423858 100644 --- a/src/Searchlight/Parsing/Token.cs +++ b/src/Searchlight/Parsing/Token.cs @@ -1,4 +1,6 @@ -namespace Searchlight.Parsing +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Searchlight.Parsing { public class Token { diff --git a/src/Searchlight/Query/MalformedClause.cs b/src/Searchlight/Query/MalformedClause.cs index a5cca1e..b02aff0 100644 --- a/src/Searchlight/Query/MalformedClause.cs +++ b/src/Searchlight/Query/MalformedClause.cs @@ -1,4 +1,6 @@ -namespace Searchlight.Query +#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member + +namespace Searchlight.Query { public class MalformedClause : BaseClause { diff --git a/tests/Searchlight.Tests/Executors/EmployeeTestSuite.cs b/tests/Searchlight.Tests/Executors/EmployeeTestSuite.cs index 901b7c2..0db1654 100644 --- a/tests/Searchlight.Tests/Executors/EmployeeTestSuite.cs +++ b/tests/Searchlight.Tests/Executors/EmployeeTestSuite.cs @@ -56,6 +56,7 @@ public static async Task BasicTestSuite(DataSource src, List list, await suite.PageNumberNoPageSize(); await suite.PageSizeNoPageNumber(); await suite.PageSizeAndPageNumber(); + await suite.EnumTranslation(); } /// @@ -630,5 +631,21 @@ private async Task PageSizeAndPageNumber() Assert.AreEqual(result.records.Length, 1); } + + private async Task EnumTranslation() + { + var syntax = _src.ParseFilter("employeetype eq FullTime"); + var result = await _executor(syntax); + Assert.AreEqual(8, result.records.Length); + + // Both string name and underlying type work + syntax = _src.ParseFilter("employeetype eq 0"); + result = await _executor(syntax); + Assert.AreEqual(8, result.records.Length); + + syntax = _src.ParseFilter("employeetype eq Contract"); + result = await _executor(syntax); + Assert.AreEqual(1, result.records.Length); + } } } \ No newline at end of file diff --git a/tests/Searchlight.Tests/Executors/LinqExecutorTests.cs b/tests/Searchlight.Tests/Executors/LinqExecutorTests.cs index 7c5f9b8..c5d8738 100644 --- a/tests/Searchlight.Tests/Executors/LinqExecutorTests.cs +++ b/tests/Searchlight.Tests/Executors/LinqExecutorTests.cs @@ -27,7 +27,7 @@ public class LinqExecutorTests public void SetupTests() { _list = EmployeeObj.GetTestList(); - _src = DataSource.Create(null, typeof(EmployeeObj), AttributeMode.Loose); + _src = DataSource.Create(null, typeof(EmployeeObj), AttributeMode.Strict); _linq = async tree => { await Task.CompletedTask; diff --git a/tests/Searchlight.Tests/Executors/MongoDBExecutorTests.cs b/tests/Searchlight.Tests/Executors/MongoDBExecutorTests.cs index 1fb5e1b..287abfb 100644 --- a/tests/Searchlight.Tests/Executors/MongoDBExecutorTests.cs +++ b/tests/Searchlight.Tests/Executors/MongoDBExecutorTests.cs @@ -25,7 +25,7 @@ public class MongoDbExecutorTests [TestInitialize] public async Task SetupMongoClient() { - _src = DataSource.Create(null, typeof(EmployeeObj), AttributeMode.Loose); + _src = DataSource.Create(null, typeof(EmployeeObj), AttributeMode.Strict); var options = new MongoRunnerOptions(); _runner = MongoRunner.Run(options); diff --git a/tests/Searchlight.Tests/Executors/MysqlExecutorTests.cs b/tests/Searchlight.Tests/Executors/MysqlExecutorTests.cs index 4a03caf..59b4fb3 100644 --- a/tests/Searchlight.Tests/Executors/MysqlExecutorTests.cs +++ b/tests/Searchlight.Tests/Executors/MysqlExecutorTests.cs @@ -21,7 +21,7 @@ public class MysqlExecutorTests [TestInitialize] public async Task SetupClient() { - _src = DataSource.Create(null, typeof(EmployeeObj), AttributeMode.Loose); + _src = DataSource.Create(null, typeof(EmployeeObj), AttributeMode.Strict); _container = new MySqlBuilder() .Build(); await _container.StartAsync(); @@ -33,7 +33,10 @@ public async Task SetupClient() await connection.OpenAsync(); // Create basic table - await using (var command = new MySqlCommand("CREATE TABLE EmployeeObj (name text null, id int not null, hired timestamp, paycheck decimal, onduty bit)", connection)) + await using (var command = + new MySqlCommand( + "CREATE TABLE EmployeeObj (name text null, id int not null, hired timestamp, paycheck decimal, onduty bit, employeetype int not null)", + connection)) { await command.ExecuteNonQueryAsync(); } @@ -41,13 +44,14 @@ public async Task SetupClient() // Insert rows foreach (var record in EmployeeObj.GetTestList()) { - await using (var command = new MySqlCommand("INSERT INTO EmployeeObj (name, id, hired, paycheck, onduty) VALUES (@name, @id, @hired, @paycheck, @onduty)", connection)) + await using (var command = new MySqlCommand("INSERT INTO EmployeeObj (name, id, hired, paycheck, onduty, employeetype) VALUES (@name, @id, @hired, @paycheck, @onduty, @employeetype)", connection)) { command.Parameters.AddWithValue("@name", (object)record.name ?? DBNull.Value); command.Parameters.AddWithValue("@id", record.id); command.Parameters.AddWithValue("@hired", record.hired); command.Parameters.AddWithValue("@paycheck", record.paycheck); command.Parameters.AddWithValue("@onduty", record.onduty); + command.Parameters.AddWithValue("@employeetype", record.employeeType); await command.ExecuteNonQueryAsync(); } } @@ -82,6 +86,7 @@ public async Task SetupClient() hired = reader.GetDateTime(2), paycheck = reader.GetDecimal(3), onduty = reader.GetBoolean(4), + employeeType = (EmployeeObj.EmployeeType)reader.GetInt32(5), }); } } diff --git a/tests/Searchlight.Tests/Executors/PostgresExecutorTests.cs b/tests/Searchlight.Tests/Executors/PostgresExecutorTests.cs index f3df2ac..c31d73d 100644 --- a/tests/Searchlight.Tests/Executors/PostgresExecutorTests.cs +++ b/tests/Searchlight.Tests/Executors/PostgresExecutorTests.cs @@ -22,7 +22,7 @@ public class PostgresExecutorTests [TestInitialize] public async Task SetupClient() { - _src = DataSource.Create(null, typeof(EmployeeObj), AttributeMode.Loose); + _src = DataSource.Create(null, typeof(EmployeeObj), AttributeMode.Strict); _container = new PostgreSqlBuilder() .Build(); await _container.StartAsync(); @@ -34,7 +34,10 @@ public async Task SetupClient() await connection.OpenAsync(); // Create basic table - await using (var command = new NpgsqlCommand("CREATE TABLE employeeobj (name text null, id int not null, hired timestamp with time zone, paycheck numeric, onduty bool)", connection)) + await using (var command = + new NpgsqlCommand( + "CREATE TABLE employeeobj (name text null, id int not null, hired timestamp with time zone, paycheck numeric, onduty bool, employeetype int not null)", + connection)) { await command.ExecuteNonQueryAsync(); } @@ -42,13 +45,14 @@ public async Task SetupClient() // Insert rows foreach (var record in EmployeeObj.GetTestList()) { - await using (var command = new NpgsqlCommand("INSERT INTO employeeobj (name, id, hired, paycheck, onduty) VALUES (@name, @id, @hired, @paycheck, @onduty)", connection)) + await using (var command = new NpgsqlCommand("INSERT INTO employeeobj (name, id, hired, paycheck, onduty, employeetype) VALUES (@name, @id, @hired, @paycheck, @onduty, @employeetype)", connection)) { command.Parameters.AddWithValue("@name", NpgsqlDbType.Text, (object)record.name ?? DBNull.Value); command.Parameters.AddWithValue("@id", NpgsqlDbType.Integer, record.id); command.Parameters.AddWithValue("@hired", NpgsqlDbType.TimestampTz, record.hired); command.Parameters.AddWithValue("@paycheck", NpgsqlDbType.Numeric, record.paycheck); command.Parameters.AddWithValue("@onduty", NpgsqlDbType.Boolean, record.onduty); + command.Parameters.AddWithValue("@employeetype", NpgsqlDbType.Integer, (int)record.employeeType); await command.ExecuteNonQueryAsync(); } } @@ -84,6 +88,7 @@ public async Task SetupClient() hired = reader.GetDateTime(2), paycheck = reader.GetDecimal(3), onduty = reader.GetBoolean(4), + employeeType = (EmployeeObj.EmployeeType)reader.GetInt32(5), }); } } @@ -108,19 +113,19 @@ private NpgsqlDbType ConvertNpgsqlType(Type parameterType) { return NpgsqlDbType.Boolean; } - else if (parameterType == typeof(string)) + if (parameterType == typeof(string)) { return NpgsqlDbType.Text; } - else if (parameterType == typeof(Int32)) + if (parameterType == typeof(Int32)) { return NpgsqlDbType.Integer; } - else if (parameterType == typeof(decimal)) + if (parameterType == typeof(decimal)) { return NpgsqlDbType.Numeric; } - else if (parameterType == typeof(DateTime)) + if (parameterType == typeof(DateTime)) { return NpgsqlDbType.TimestampTz; } diff --git a/tests/Searchlight.Tests/Executors/SqlServerExecutorTests.cs b/tests/Searchlight.Tests/Executors/SqlServerExecutorTests.cs index 7981e32..c00de3a 100644 --- a/tests/Searchlight.Tests/Executors/SqlServerExecutorTests.cs +++ b/tests/Searchlight.Tests/Executors/SqlServerExecutorTests.cs @@ -22,7 +22,7 @@ public class SqlServerExecutorTests [TestInitialize] public async Task SetupClient() { - _src = DataSource.Create(null, typeof(EmployeeObj), AttributeMode.Loose); + _src = DataSource.Create(null, typeof(EmployeeObj), AttributeMode.Strict); _container = new MsSqlBuilder() .Build(); await _container.StartAsync(); @@ -32,9 +32,12 @@ public async Task SetupClient() using (var connection = new SqlConnection(_connectionString)) { await connection.OpenAsync(); - + // Create basic table - using (var command = new SqlCommand("CREATE TABLE employeeobj (name nvarchar(255) null, id int not null, hired datetime not null, paycheck decimal not null, onduty bit not null)", connection)) + using (var command = + new SqlCommand( + "CREATE TABLE employeeobj (name nvarchar(255) null, id int not null, hired datetime not null, paycheck decimal not null, onduty bit not null, employeetype int null DEFAULT 0)", + connection)) { await command.ExecuteNonQueryAsync(); } @@ -42,13 +45,14 @@ public async Task SetupClient() // Insert rows foreach (var record in EmployeeObj.GetTestList()) { - using (var command = new SqlCommand("INSERT INTO employeeobj (name, id, hired, paycheck, onduty) VALUES (@name, @id, @hired, @paycheck, @onduty)", connection)) + using (var command = new SqlCommand("INSERT INTO employeeobj (name, id, hired, paycheck, onduty, employeetype) VALUES (@name, @id, @hired, @paycheck, @onduty, @employeetype)", connection)) { command.Parameters.AddWithValue("@name", (object)record.name ?? DBNull.Value); command.Parameters.AddWithValue("@id", record.id); command.Parameters.AddWithValue("@hired", record.hired); command.Parameters.AddWithValue("@paycheck", record.paycheck); command.Parameters.AddWithValue("@onduty", record.onduty); + command.Parameters.AddWithValue("@employeetype", record.employeeType); await command.ExecuteNonQueryAsync(); } } @@ -96,6 +100,7 @@ public async Task SetupClient() hired = reader.GetDateTime("hired"), paycheck = reader.GetDecimal("paycheck"), onduty = reader.GetBoolean("onduty"), + employeeType = (EmployeeObj.EmployeeType)reader.GetInt32(5), }); } } diff --git a/tests/Searchlight.Tests/Models/EmployeeObj.cs b/tests/Searchlight.Tests/Models/EmployeeObj.cs index bd7e145..4cb354f 100644 --- a/tests/Searchlight.Tests/Models/EmployeeObj.cs +++ b/tests/Searchlight.Tests/Models/EmployeeObj.cs @@ -6,147 +6,188 @@ // ReSharper disable InconsistentNaming -namespace Searchlight.Tests.Models +namespace Searchlight.Tests.Models; + +[SearchlightModel(DefaultSort = nameof(name))] +public class EmployeeObj { - [SearchlightModel(DefaultSort = nameof(name))] - public class EmployeeObj + public enum EmployeeType { - public string name { get; set; } - public int id { get; set; } - public DateTime hired { get; set; } - [BsonRepresentation(BsonType.Decimal128)] - public decimal paycheck { get; set; } - public bool onduty { get; set; } + FullTime, + PartTime, + Contract + } + + [SearchlightField(FieldType = typeof(string))] + public string name { get; set; } + + [SearchlightField(FieldType = typeof(int))] + public int id { get; set; } + + [SearchlightField(FieldType = typeof(DateTime))] + public DateTime hired { get; set; } + + [BsonRepresentation(BsonType.Decimal128)] + [SearchlightField(FieldType = typeof(decimal))] + public decimal paycheck { get; set; } + + [SearchlightField(FieldType = typeof(bool))] + public bool onduty { get; set; } + + [BsonRepresentation(BsonType.Int32)] + [SearchlightField(FieldType = typeof(int), EnumType = typeof(EmployeeType))] + public EmployeeType employeeType { get; set; } - public static List GetTestList() + public static List GetTestList() + { + return new List { - return new List + new() + { + hired = DateTime.UtcNow.AddMinutes(-1), + id = 1, + name = "Alice Smith", + onduty = true, + paycheck = 1000.00m, + employeeType = EmployeeType.FullTime, + }, + new() + { + hired = DateTime.UtcNow.AddMonths(-1), + id = 2, + name = "Bob Rogers", + onduty = true, + paycheck = 1000.00m, + employeeType = EmployeeType.PartTime, + }, + new() + { + hired = DateTime.UtcNow.AddMonths(-6), + id = 3, + name = "Charlie Prentiss", + onduty = false, + paycheck = 800.0m, + employeeType = EmployeeType.Contract, + }, + new() + { + hired = DateTime.UtcNow.AddMonths(-12), + id = 4, + name = "Danielle O'Shea", + onduty = false, + paycheck = 1200.0m, + employeeType = EmployeeType.FullTime, + }, + new() + { + hired = DateTime.UtcNow.AddMonths(1), + id = 5, + name = "Ernest Nofzinger", + onduty = true, + paycheck = 1000.00m, + employeeType = EmployeeType.FullTime, + }, + new() + { + hired = DateTime.UtcNow.AddMonths(4), + id = 6, + name = null, + onduty = false, + paycheck = 10.00m, + employeeType = EmployeeType.FullTime, + }, + new() + { + hired = DateTime.UtcNow.AddMonths(2), + id = 7, + name = "Roderick 'null' Sqlkeywordtest", + onduty = false, + paycheck = 578.00m, + employeeType = EmployeeType.FullTime, + }, + new() { - new() - { hired = DateTime.UtcNow.AddMinutes(-1), id = 1, name = "Alice Smith", onduty = true, paycheck = 1000.00m }, - new() - { - hired = DateTime.UtcNow.AddMonths(-1), - id = 2, - name = "Bob Rogers", - onduty = true, - paycheck = 1000.00m - }, - new() - { - hired = DateTime.UtcNow.AddMonths(-6), - id = 3, - name = "Charlie Prentiss", - onduty = false, - paycheck = 800.0m - }, - new() - { - hired = DateTime.UtcNow.AddMonths(-12), - id = 4, - name = "Danielle O'Shea", - onduty = false, - paycheck = 1200.0m - }, - new() - { - hired = DateTime.UtcNow.AddMonths(1), - id = 5, - name = "Ernest Nofzinger", - onduty = true, - paycheck = 1000.00m - }, - new() - { hired = DateTime.UtcNow.AddMonths(4), id = 6, name = null, onduty = false, paycheck = 10.00m }, - new() - { - hired = DateTime.UtcNow.AddMonths(2), - id = 7, - name = "Roderick 'null' Sqlkeywordtest", - onduty = false, - paycheck = 578.00m - }, - new() - { - hired = DateTime.UtcNow.AddHours(-1), - id = 8, - name = "Joe 'Fresh Hire' McGillicuddy", - onduty = false, - paycheck = 123.00m, - }, - new() - { - hired = DateTime.UtcNow.AddHours(1), - id = 9, - name = "Carol 'Starting Soon!' Yamashita", - onduty = false, - paycheck = 987.00m, - }, - new() - { - hired = DateTime.UtcNow.AddHours(15), - id = 10, - name = "Barnabas '[Not.Regex(safe{\\^|$' Ellsworth", - onduty = true, - paycheck = 632.00m, - } - }; - } + hired = DateTime.UtcNow.AddHours(-1), + id = 8, + name = "Joe 'Fresh Hire' McGillicuddy", + onduty = false, + paycheck = 123.00m, + employeeType = EmployeeType.FullTime, + }, + new() + { + hired = DateTime.UtcNow.AddHours(1), + id = 9, + name = "Carol 'Starting Soon!' Yamashita", + onduty = false, + paycheck = 987.00m, + employeeType = EmployeeType.FullTime, + }, + new() + { + hired = DateTime.UtcNow.AddHours(15), + id = 10, + name = "Barnabas '[Not.Regex(safe{\\^|$' Ellsworth", + onduty = true, + paycheck = 632.00m, + employeeType = EmployeeType.FullTime, + } + }; } +} - public class CompatibleEmployeeObj - { - public string name { get; set; } - public int? id { get; set; } - public string hired { get; set; } - public string paycheck { get; set; } - public bool onduty { get; set; } +public class CompatibleEmployeeObj +{ + public string name { get; set; } + public int? id { get; set; } + public string hired { get; set; } + public string paycheck { get; set; } + public bool onduty { get; set; } - public static List GetCompatibleList() + public static List GetCompatibleList() + { + return new List() { - return new List() + new CompatibleEmployeeObj() { - new CompatibleEmployeeObj() - { - name = "Charlie Compatible", - id = 57, - hired = "true", - paycheck = "$1000.00", - onduty = false - }, - new CompatibleEmployeeObj() - { - name = "Nelly Null", - id = null, - hired = null, - paycheck = null, - onduty = false - }, - }; - } + name = "Charlie Compatible", + id = 57, + hired = "true", + paycheck = "$1000.00", + onduty = false + }, + new CompatibleEmployeeObj() + { + name = "Nelly Null", + id = null, + hired = null, + paycheck = null, + onduty = false + }, + }; } +} - public class IncompatibleEmployeeObj - { - public string FullName { get; set; } - public int Identifier { get; set; } - public decimal? IncompatibleMongoField { get; set; } +public class IncompatibleEmployeeObj +{ + public string FullName { get; set; } + public int Identifier { get; set; } + public decimal? IncompatibleMongoField { get; set; } - public static List GetIncompatibleList() + public static List GetIncompatibleList() + { + return new List() { - return new List() + new IncompatibleEmployeeObj() + { + FullName = "Irving Incompatible", + Identifier = 1, + }, + new IncompatibleEmployeeObj() { - new IncompatibleEmployeeObj() - { - FullName = "Irving Incompatible", - Identifier = 1, - }, - new IncompatibleEmployeeObj() - { - FullName = "Noreen Negative", - Identifier = -1, - } - }; - } + FullName = "Noreen Negative", + Identifier = -1, + } + }; } } \ No newline at end of file diff --git a/tests/Searchlight.Tests/ParseModelTests.cs b/tests/Searchlight.Tests/ParseModelTests.cs index 2fb7379..132f431 100644 --- a/tests/Searchlight.Tests/ParseModelTests.cs +++ b/tests/Searchlight.Tests/ParseModelTests.cs @@ -234,15 +234,7 @@ public void TestEngineAssemblyParsing() Assert.IsNotNull(engine.FindTable("BookCopy")); // This is the list of expected errors - Assert.AreEqual(4, engine.ModelErrors.Count); - Assert.IsTrue(engine.ModelErrors.Any(err => - { - if (err is InvalidDefaultSort defSort) - { - return (defSort.Table == "EmployeeObj"); - } - return false; - })); + Assert.AreEqual(3, engine.ModelErrors.Count); Assert.IsTrue(engine.ModelErrors.Any(err => { if (err is DuplicateName duplicateName) @@ -508,5 +500,48 @@ public void InconsistentCompoundClause() Assert.AreEqual("Name Equals Alice OR Name Equals Bob AND Description Contains employee", ex2.InconsistentClause); Assert.IsTrue(ex2.ErrorMessage.StartsWith("Mixing AND and OR conjunctions in the same statement results in an imprecise query.")); } + + public enum TestEnumValueCategory + { + None = 0, + Special = 1, + Generic = 2, + } + + [SearchlightModel(DefaultSort = "Name ascending")] + public class TestClassWithEnumValues + { + [SearchlightField(OriginalName = "field_name")] + public string Name { get; set; } + [SearchlightField(OriginalName = "field_category")] + public TestEnumValueCategory Category { get; set; } + } + + + [TestMethod] + public void TestValidEnumFilters() + { + var source = DataSource.Create(null, typeof(TestClassWithEnumValues), AttributeMode.Strict); + var columns = source.GetColumnDefinitions().ToArray(); + Assert.AreEqual(2, 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); + + // Query for a valid category + var syntax1 = source.ParseFilter("category = None"); + Assert.IsNotNull(syntax1); + + // 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 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); + } } } \ No newline at end of file