Skip to content

Commit f5fc02d

Browse files
authored
feat: add soundex options (#8)
* fix: flakey test * feat: basic equals soundex * feat: supports does not sound like * feat: xml comments on public methods * docs: soundex * update: more explicit dbcontext usage and support no good way to get dbcontext from an iqueryable :-( * feat: soundex exceptions * refactor: reset testing migration
1 parent 1c1c9db commit f5fc02d

14 files changed

Lines changed: 272 additions & 33 deletions

QueryKit.IntegrationTests/Tests/DatabaseFilteringTests.cs

Lines changed: 56 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ namespace QueryKit.IntegrationTests.Tests;
88
using SharedTestingHelper.Fakes;
99
using SharedTestingHelper.Fakes.Author;
1010
using SharedTestingHelper.Fakes.Recipes;
11+
using WebApiTestProject.Database;
1112
using WebApiTestProject.Entities;
1213
using WebApiTestProject.Entities.Recipes;
1314
using WebApiTestProject.Features;
@@ -38,6 +39,59 @@ public async Task can_filter_by_string()
3839
people[0].Id.Should().Be(fakePersonOne.Id);
3940
}
4041

42+
[Fact]
43+
public async Task can_use_soundex_equals()
44+
{
45+
// Arrange
46+
var testingServiceScope = new TestingServiceScope();
47+
48+
var fakePersonOne = new FakeTestingPersonBuilder()
49+
.WithTitle("DeVito")
50+
.Build();
51+
var fakePersonTwo = new FakeTestingPersonBuilder().Build();
52+
await testingServiceScope.InsertAsync(fakePersonOne, fakePersonTwo);
53+
54+
var input = $"""{nameof(TestingPerson.Title)} ~~ "davito" """;
55+
56+
// Act
57+
var queryablePeople = testingServiceScope.DbContext().People;
58+
var appliedQueryable = queryablePeople.ApplyQueryKitFilter(input, new QueryKitConfiguration(o =>
59+
{
60+
o.DbContextType = typeof(TestingDbContext);
61+
}));
62+
var people = await appliedQueryable.ToListAsync();
63+
64+
// Assert
65+
people.Count.Should().Be(1);
66+
people[0].Id.Should().Be(fakePersonOne.Id);
67+
}
68+
69+
[Fact]
70+
public async Task can_use_soundex_not_equals()
71+
{
72+
// Arrange
73+
var testingServiceScope = new TestingServiceScope();
74+
75+
var fakePersonOne = new FakeTestingPersonBuilder()
76+
.WithTitle("Jaime")
77+
.Build();
78+
var fakePersonTwo = new FakeTestingPersonBuilder().Build();
79+
await testingServiceScope.InsertAsync(fakePersonOne, fakePersonTwo);
80+
81+
var input = $"""{nameof(TestingPerson.Title)} !~ "jaymee" """;
82+
83+
// Act
84+
var queryablePeople = testingServiceScope.DbContext().People;
85+
var appliedQueryable = queryablePeople.ApplyQueryKitFilter(input, new QueryKitConfiguration(o =>
86+
{
87+
o.DbContextType = typeof(TestingDbContext);
88+
}));
89+
var people = await appliedQueryable.ToListAsync();
90+
91+
// Assert
92+
people.Count(x => x.Id == fakePersonOne.Id).Should().Be(0);
93+
}
94+
4195
[Fact]
4296
public async Task can_filter_by_datetime_with_milliseconds()
4397
{
@@ -144,8 +198,8 @@ public async Task can_filter_by_timeonly_without_micros()
144198
var people = await appliedQueryable.ToListAsync();
145199

146200
// Assert
147-
people.Count.Should().Be(1);
148-
people[0].Id.Should().Be(fakePersonOne.Id);
201+
people.Count.Should().BeGreaterOrEqualTo(1);
202+
people.FirstOrDefault(x => x.Id == fakePersonOne.Id).Should().NotBeNull();
149203
}
150204

151205

QueryKit.WebApiTestProject/Database/TestingDbContext.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,18 @@ public TestingDbContext(DbContextOptions<TestingDbContext> options)
1010
: base(options)
1111
{
1212
}
13+
14+
[DbFunction (Name = "SOUNDEX", IsBuiltIn = true)]
15+
public static string SoundsLike(string query) => throw new NotImplementedException();
1316

1417
public DbSet<TestingPerson> People { get; set; }
1518
public DbSet<Recipe> Recipes { get; set; }
1619

1720
protected override void OnModelCreating(ModelBuilder modelBuilder)
1821
{
1922
base.OnModelCreating(modelBuilder);
20-
23+
modelBuilder.HasPostgresExtension("fuzzystrmatch");
24+
2125
modelBuilder.ApplyConfiguration(new PersonConfiguration());
2226
modelBuilder.ApplyConfiguration(new RecipeConfiguration());
2327
}

QueryKit.WebApiTestProject/Migrations/20230501005146_initial.Designer.cs renamed to QueryKit.WebApiTestProject/Migrations/20230718122822_BaseTestingMigration.Designer.cs

Lines changed: 3 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

QueryKit.WebApiTestProject/Migrations/20230501005146_initial.cs renamed to QueryKit.WebApiTestProject/Migrations/20230718122822_BaseTestingMigration.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,14 @@
66
namespace QueryKit.WebApiTestProject.Migrations
77
{
88
/// <inheritdoc />
9-
public partial class initial : Migration
9+
public partial class BaseTestingMigration : Migration
1010
{
1111
/// <inheritdoc />
1212
protected override void Up(MigrationBuilder migrationBuilder)
1313
{
14+
migrationBuilder.AlterDatabase()
15+
.Annotation("Npgsql:PostgresExtension:fuzzystrmatch", ",,");
16+
1417
migrationBuilder.CreateTable(
1518
name: "people",
1619
columns: table => new

QueryKit.WebApiTestProject/Migrations/TestingDbContextModelSnapshot.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ protected override void BuildModel(ModelBuilder modelBuilder)
2020
.HasAnnotation("ProductVersion", "7.0.5")
2121
.HasAnnotation("Relational:MaxIdentifierLength", 63);
2222

23+
NpgsqlModelBuilderExtensions.HasPostgresExtension(modelBuilder, "fuzzystrmatch");
2324
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
2425

2526
modelBuilder.Entity("QueryKit.WebApiTestProject.Entities.Authors.Author", b =>

QueryKit/Configuration/QueryKitConfiguration.cs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
namespace QueryKit.Configuration;
22

3+
using Microsoft.EntityFrameworkCore;
4+
35
public interface IQueryKitConfiguration
46
{
57
QueryKitPropertyMappings PropertyMappings { get; }
@@ -16,10 +18,13 @@ public interface IQueryKitConfiguration
1618
public string NotStartsWithOperator { get; set; }
1719
public string NotEndsWithOperator { get; set; }
1820
public string InOperator { get; set; }
21+
public string SoundsLikeOperator { get; set; }
22+
public string DoesNotSoundLikeOperator { get; set; }
1923
public string CaseInsensitiveAppendix { get; set; }
2024
public string AndOperator { get; set; }
2125
public string OrOperator { get; set; }
2226
public bool AllowUnknownProperties { get; set; }
27+
public Type? DbContextType { get; set; }
2328
}
2429

2530
public class QueryKitConfiguration : IQueryKitConfiguration
@@ -38,11 +43,14 @@ public class QueryKitConfiguration : IQueryKitConfiguration
3843
public string NotStartsWithOperator { get; set; }
3944
public string NotEndsWithOperator { get; set; }
4045
public string InOperator { get; set; }
46+
public string SoundsLikeOperator { get; set; }
47+
public string DoesNotSoundLikeOperator { get; set; }
4148
public string CaseInsensitiveAppendix { get; set; }
4249
public string AndOperator { get; set; }
4350
public string OrOperator { get; set; }
4451
public bool AllowUnknownProperties { get; set; } = false;
45-
52+
public Type? DbContextType { get; set; }
53+
4654
public QueryKitConfiguration(Action<QueryKitSettings> configureSettings)
4755
{
4856
var settings = new QueryKitSettings();
@@ -62,9 +70,12 @@ public QueryKitConfiguration(Action<QueryKitSettings> configureSettings)
6270
NotStartsWithOperator = settings.NotStartsWithOperator;
6371
NotEndsWithOperator = settings.NotEndsWithOperator;
6472
InOperator = settings.InOperator;
73+
SoundsLikeOperator = settings.SoundsLikeOperator;
74+
DoesNotSoundLikeOperator = settings.DoesNotSoundLikeOperator;
6575
CaseInsensitiveAppendix = settings.CaseInsensitiveAppendix;
6676
AndOperator = settings.AndOperator;
6777
OrOperator = settings.OrOperator;
6878
AllowUnknownProperties = settings.AllowUnknownProperties;
79+
DbContextType = settings.DbContextType;
6980
}
7081
}

QueryKit/Configuration/QueryKitSettings.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ public class QueryKitSettings
1919
public string NotStartsWithOperator { get; set; } = ComparisonOperator.NotStartsWithOperator().Operator();
2020
public string NotEndsWithOperator { get; set; } = ComparisonOperator.NotEndsWithOperator().Operator();
2121
public string InOperator { get; set; } = ComparisonOperator.InOperator().Operator();
22+
public string SoundsLikeOperator { get; set; } = ComparisonOperator.SoundsLikeOperator().Operator();
23+
public string DoesNotSoundLikeOperator { get; set; } = ComparisonOperator.DoesNotSoundLikeOperator().Operator();
2224
public string AndOperator { get; set; } = LogicalOperator.AndOperator.Operator();
2325
public string OrOperator { get; set; } = LogicalOperator.OrOperator.Operator();
2426
public string CaseInsensitiveAppendix { get; set; } = ComparisonOperator.CaseSensitiveAppendix.ToString();
2527
public bool AllowUnknownProperties { get; set; }
26-
28+
public Type? DbContextType { get; set; }
29+
2730
public QueryKitPropertyMapping<TModel> Property<TModel>(Expression<Func<TModel, object>>? propertySelector)
2831
{
2932
return PropertyMappings.Property(propertySelector);
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace QueryKit.Exceptions;
2+
3+
public sealed class QueryKitDbContextTypeException : Exception
4+
{
5+
public QueryKitDbContextTypeException(string specificMessage)
6+
: base($"There no DbContext type provided in your QueryKit config, but one was needed for this operation. {specificMessage}")
7+
{
8+
}
9+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
namespace QueryKit.Exceptions;
2+
3+
public sealed class SoundsLikeNotImplementedException : Exception
4+
{
5+
public SoundsLikeNotImplementedException(string dbContextType)
6+
: base($"The DbContext type {dbContextType} does not have a SoundsLike method.")
7+
{
8+
}
9+
}

QueryKit/FilterParser.cs

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,19 @@ namespace QueryKit;
66
using System.Reflection;
77
using Configuration;
88
using Exceptions;
9+
using Microsoft.EntityFrameworkCore;
910
using Operators;
1011
using Sprache;
1112

1213
public static class FilterParser
1314
{
15+
/// <summary>
16+
/// Generates an expression parser to filter data of the specified type.
17+
/// </summary>
18+
/// <param name="input">A string that defines the filter parameters.</param>
19+
/// <param name="config">An optional IQueryKitConfiguration object to provide configuration for parsing, including logical aliases, comparison aliases and property mappings. Defaults to null.</param>
20+
/// <typeparam name="T">The type of data to be filtered by the returned expression parser.</typeparam>
21+
/// <returns>Returns a Func delegate that represents a lambda expression that applies the filter defined by the input parameter.</returns>
1422
public static Expression<Func<T, bool>> ParseFilter<T>(string input, IQueryKitConfiguration? config = null)
1523
{
1624
input = config?.ReplaceLogicalAliases(input) ?? input;
@@ -49,6 +57,8 @@ from rest in Parse.LetterOrDigit.XOr(Parse.Char('_')).Many()
4957
.Or(Parse.String(ComparisonOperator.NotStartsWithOperator().Operator()).Text())
5058
.Or(Parse.String(ComparisonOperator.NotEndsWithOperator().Operator()).Text())
5159
.Or(Parse.String(ComparisonOperator.InOperator().Operator()).Text())
60+
.Or(Parse.String(ComparisonOperator.SoundsLikeOperator().Operator()).Text())
61+
.Or(Parse.String(ComparisonOperator.DoesNotSoundLikeOperator().Operator()).Text())
5262
.SelectMany(op => Parse.Char(ComparisonOperator.CaseSensitiveAppendix).Optional(), (op, caseInsensitive) => new { op, caseInsensitive })
5363
.Select(x => ComparisonOperator.GetByOperatorString(x.op, x.caseInsensitive.IsDefined));
5464

@@ -296,7 +306,7 @@ private static Parser<Expression> ComparisonExprParser<T>(ParameterExpression pa
296306
}
297307

298308
var rightExpr = CreateRightExpr(temp.leftExpr, temp.right);
299-
return temp.op.GetExpression<T>(temp.leftExpr, rightExpr);
309+
return temp.op.GetExpression<T>(temp.leftExpr, rightExpr, config?.DbContextType);
300310
});
301311
}
302312

@@ -337,8 +347,7 @@ private static Parser<Expression> ComparisonExprParser<T>(ParameterExpression pa
337347
});
338348
}
339349

340-
private static Parser<Expression> AtomicExprParser<T>(ParameterExpression parameter,
341-
IQueryKitConfiguration? config = null)
350+
private static Parser<Expression> AtomicExprParser<T>(ParameterExpression parameter, IQueryKitConfiguration? config = null)
342351
=> ComparisonExprParser<T>(parameter, config)
343352
.Or(Parse.Ref(() => ExprParser<T>(parameter, config)).Contained(Parse.Char('('), Parse.Char(')')));
344353

0 commit comments

Comments
 (0)