Skip to content

Commit

Permalink
Translate enum string name to underlying enum type (#137)
Browse files Browse the repository at this point in the history
* Add !=, In, IsNull, starts/endswith, fix contains and test

* default for default sort

* initial changes for enum translation

* add test cases, remove error since default sort added

* update container tests, formatting, fix enum parse logic

* typo!

* fix column type, add reader for enum column

* Update code with revisions from autocomplete

---------

Co-authored-by: Charlie Taylor <[email protected]>
Co-authored-by: Charlie Taylor <[email protected]>
Co-authored-by: Charlie Taylor <[email protected]>
Co-authored-by: Ted Spence <[email protected]>
  • Loading branch information
5 people authored Sep 10, 2023
1 parent 01785ef commit 70b0e0d
Show file tree
Hide file tree
Showing 17 changed files with 327 additions and 169 deletions.
11 changes: 5 additions & 6 deletions Searchlight.nuspec
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<package >
<metadata>
<id>Searchlight</id>
<version>1.0.1</version>
<version>1.0.2</version>
<title>Searchlight</title>
<authors>Ted Spence</authors>
<owners>Ted Spence</owners>
Expand All @@ -17,12 +17,11 @@
<summary>A friendly and readable domain specific query language for your API.</summary>
<icon>docs/icons8-searchlight-90.png</icon>
<releaseNotes>
# 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.
</releaseNotes>
<copyright>Copyright 2013 - 2023</copyright>
<tags>REST query language abstract syntax tree parser sql-injection protection friendly readable domain-specific DSL</tags>
Expand Down
1 change: 1 addition & 0 deletions src/Searchlight/Autocomplete/CompletionList.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Collections.Generic;
#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member

namespace Searchlight.Autocomplete
{
Expand Down
13 changes: 7 additions & 6 deletions src/Searchlight/DataSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,15 +65,15 @@ public class DataSource
/// <returns></returns>
public DataSource WithColumn(string columnName, Type columnType)
{
return WithRenamingColumn(columnName, columnName, null, columnType, null);
return WithRenamingColumn(columnName, columnName, null, columnType, null, null);
}

/// <summary>
/// Add a column to this definition
/// </summary>
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
Expand Down Expand Up @@ -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<string>(), t, filter.Description);
var t = filter.FieldType ?? pi.PropertyType;
var columnName = filter.OriginalName ?? pi.Name;
var aliases = filter.Aliases ?? Array.Empty<string>();
src.WithRenamingColumn(pi.Name, columnName, aliases, t, filter.EnumType, filter.Description);
}

var collection = pi.GetCustomAttributes<SearchlightCollection>().FirstOrDefault();
Expand Down
4 changes: 3 additions & 1 deletion src/Searchlight/Exceptions/TableNotFoundException.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down
6 changes: 6 additions & 0 deletions src/Searchlight/LinqExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ private static Expression BuildOneExpression<T>(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)
{
Expand Down
16 changes: 15 additions & 1 deletion src/Searchlight/Parsing/ColumnInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,20 @@ public class ColumnInfo
/// <param name="columnName">The name of the column in the database</param>
/// <param name="aliases">If this field is known by other names, list them here</param>
/// <param name="columnType">The raw type of the column in the database</param>
public ColumnInfo(string filterName, string columnName, string[] aliases, Type columnType, string description)
/// <param name="enumType">The type of the enum that the column is mapped to</param>
/// <param name="description">A description of the column for autocomplete</param>
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;
}

Expand All @@ -43,6 +51,12 @@ public ColumnInfo(string filterName, string columnName, string[] aliases, Type c
/// </summary>
public Type FieldType { get; private set; }

/// <summary>
/// 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
/// </summary>
public Type EnumType { get; private set; }

/// <summary>
/// Detailed field documentation for autocomplete, if provided.
/// </summary>
Expand Down
25 changes: 24 additions & 1 deletion src/Searchlight/Parsing/SyntaxParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@

namespace Searchlight.Parsing
{
/// <summary>
/// Static class for syntax parsing
/// </summary>
public static class SyntaxParser
{
/// <summary>
Expand Down Expand Up @@ -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));
}
Expand Down
4 changes: 3 additions & 1 deletion src/Searchlight/Parsing/Token.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down
4 changes: 3 additions & 1 deletion src/Searchlight/Query/MalformedClause.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down
17 changes: 17 additions & 0 deletions tests/Searchlight.Tests/Executors/EmployeeTestSuite.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public static async Task BasicTestSuite(DataSource src, List<EmployeeObj> list,
await suite.PageNumberNoPageSize();
await suite.PageSizeNoPageNumber();
await suite.PageSizeAndPageNumber();
await suite.EnumTranslation();
}

/// <summary>
Expand Down Expand Up @@ -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);
}
}
}
2 changes: 1 addition & 1 deletion tests/Searchlight.Tests/Executors/LinqExecutorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion tests/Searchlight.Tests/Executors/MongoDBExecutorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
11 changes: 8 additions & 3 deletions tests/Searchlight.Tests/Executors/MysqlExecutorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -33,21 +33,25 @@ 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();
}

// 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();
}
}
Expand Down Expand Up @@ -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),
});
}
}
Expand Down
19 changes: 12 additions & 7 deletions tests/Searchlight.Tests/Executors/PostgresExecutorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -34,21 +34,25 @@ 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();
}

// 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();
}
}
Expand Down Expand Up @@ -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),
});
}
}
Expand All @@ -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;
}
Expand Down
13 changes: 9 additions & 4 deletions tests/Searchlight.Tests/Executors/SqlServerExecutorTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -32,23 +32,27 @@ 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();
}

// 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();
}
}
Expand Down Expand Up @@ -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),
});
}
}
Expand Down
Loading

0 comments on commit 70b0e0d

Please sign in to comment.