diff --git a/QueryKit.IntegrationTests/Tests/FilterParsingRegressionTests.cs b/QueryKit.IntegrationTests/Tests/FilterParsingRegressionTests.cs index a2269c3..49782b1 100644 --- a/QueryKit.IntegrationTests/Tests/FilterParsingRegressionTests.cs +++ b/QueryKit.IntegrationTests/Tests/FilterParsingRegressionTests.cs @@ -16,7 +16,7 @@ namespace QueryKit.IntegrationTests.Tests; public class FilterParsingRegressionTests : TestBase { [Fact] - public async Task operator_alias_text_inside_quoted_value_is_replaced() + public async Task operator_alias_text_inside_quoted_value_is_kept() { // Arrange var testingServiceScope = new TestingServiceScope(); @@ -42,7 +42,8 @@ public async Task operator_alias_text_inside_quoted_value_is_replaced() var people = await appliedQueryable.ToListAsync(); // Assert - people.Should().BeEmpty(); + people.Count.Should().Be(1); + people[0].Id.Should().Be(fakePersonOne.Id); } [Fact] diff --git a/QueryKit.UnitTests/OperatorAliasTests.cs b/QueryKit.UnitTests/OperatorAliasTests.cs index 68c83ff..76a3a24 100644 --- a/QueryKit.UnitTests/OperatorAliasTests.cs +++ b/QueryKit.UnitTests/OperatorAliasTests.cs @@ -205,27 +205,27 @@ public void can_use_ints() } [Fact] - public void alias_text_inside_quoted_value_is_replaced() + public void alias_text_inside_quoted_value_is_not_replaced() { var input = """Title eq "salt and pepper or eq gt" and Rating gt 3"""; var config = new CustomQueryKitConfiguration(); var filterExpression = FilterParser.ParseFilter(input, config); - filterExpression.ToDisplayString().Should().Be($"""x => ((x.Title == "salt && pepper || == gt") AndAlso (x.Rating > 3))"""); + filterExpression.ToDisplayString().Should().Be($"""x => ((x.Title == "salt and pepper or eq gt") AndAlso (x.Rating > 3))"""); } [Fact] - public void case_insensitive_alias_text_inside_quoted_value_is_replaced() + public void case_insensitive_alias_text_inside_quoted_value_is_not_replaced() { var input = """Title eqi "Whisk AND fry" """; var config = new CustomQueryKitConfiguration(); var filterExpression = FilterParser.ParseFilter(input, config); - filterExpression.ToDisplayString().Should().Be($"""x => ((x.Title != null) AndAlso (x.Title.ToLower() == "Whisk && fry".ToLower()))"""); + filterExpression.ToDisplayString().Should().Be($"""x => ((x.Title != null) AndAlso (x.Title.ToLower() == "Whisk AND fry".ToLower()))"""); } [Fact] - public void can_use_alias_operator_with_query_name_and_alias_text_in_the_value_is_replaced() + public void can_use_alias_operator_with_query_name() { var input = """name eq "a eq b" """; @@ -234,7 +234,20 @@ public void can_use_alias_operator_with_query_name_and_alias_text_in_the_value_i settings.Property(x => x.Title).HasQueryName("name"); }); var filterExpression = FilterParser.ParseFilter(input, config); - filterExpression.ToDisplayString().Should().Be($"""x => (x.Title == "a == b")"""); + filterExpression.ToDisplayString().Should().Be($"""x => (x.Title == "a eq b")"""); + } + + [Fact] + public void query_name_and_alias_text_inside_quoted_value_is_not_replaced() + { + var input = """name eq "name eq x" """; + + var config = new CustomQueryKitConfiguration(settings => + { + settings.Property(x => x.Title).HasQueryName("name"); + }); + var filterExpression = FilterParser.ParseFilter(input, config); + filterExpression.ToDisplayString().Should().Be($"""x => (x.Title == "name eq x")"""); } public class CustomQueryKitConfiguration : QueryKitConfiguration diff --git a/QueryKit/Configuration/QueryKitConfigurationExtensions.cs b/QueryKit/Configuration/QueryKitConfigurationExtensions.cs index 43735b6..f025780 100644 --- a/QueryKit/Configuration/QueryKitConfigurationExtensions.cs +++ b/QueryKit/Configuration/QueryKitConfigurationExtensions.cs @@ -1,37 +1,9 @@ namespace QueryKit.Configuration; -using System.Text.RegularExpressions; using QueryKit.Exceptions; -using QueryKit.Operators; internal static class QueryKitConfigurationExtensions { - internal static string ReplaceComparisonAliases(this IQueryKitConfiguration configuration, string input) - { - var aliasedOperators = ComparisonOperator.GetAliasMatches(configuration); - foreach (var comparisonAliasMatch in aliasedOperators) - { - var escapedAlias = Regex.Escape(comparisonAliasMatch.Alias); - var regex = AliasRegexCache.Get($@"(?<=\s|^){escapedAlias}(?=\s|$)"); - input = regex.Replace(input, comparisonAliasMatch.Operator); - } - - return input; - } - - internal static string ReplaceLogicalAliases(this IQueryKitConfiguration configuration, string input) - { - var aliasedOperators = LogicalOperator.GetAliasMatches(configuration); - foreach (var logicalAliasMatch in aliasedOperators) - { - var escapedAlias = Regex.Escape(logicalAliasMatch.Alias); - var regex = AliasRegexCache.Get($@"(?<=\s|^){escapedAlias}(?=\s|$)"); - input = regex.Replace(input, logicalAliasMatch.Operator); - } - - return input; - } - internal static string? GetPropertyPathByQueryName(this IQueryKitConfiguration configuration, string? queryName) { return configuration.PropertyMappings.GetPropertyPathByQueryName(queryName); diff --git a/QueryKit/FilterParser.cs b/QueryKit/FilterParser.cs index 569800a..c1c849b 100644 --- a/QueryKit/FilterParser.cs +++ b/QueryKit/FilterParser.cs @@ -21,9 +21,6 @@ public static class FilterParser public static Expression> ParseFilter(string input, IQueryKitConfiguration? config = null) { EnsureWithinInputLength(input, config); - - input = config?.ReplaceLogicalAliases(input) ?? input; - input = config?.ReplaceComparisonAliases(input) ?? input; var parameter = Expression.Parameter(typeof(T), "x"); Expression expr; @@ -238,13 +235,12 @@ private static Parser> PropertyListParser(Parser pro private static Parser ComparisonOperatorParser(IQueryKitConfiguration? config) { - var operatorParser = CanonicalComparisonOperatorParser.Or(ComparisonOperatorAliasParser(config)); + var operatorParser = ComparisonOperatorAliasParser(config).Or(CanonicalComparisonOperatorParser); return Parse.Char(ComparisonOperator.AllPrefix).Optional().Select(opt => opt.IsDefined) .Then(hasHash => operatorParser.Select(x => ComparisonOperator.GetByOperatorString(x.Operator, x.CaseInsensitive, hasHash))); } - // The rewrite before the parse replaces each alias that stands between whitespace, like v1.14.2. - // The grammar reads an alias that the rewrite did not replace, for example `(Age)eq 3`. + // Aliases are matched in the grammar (not by rewriting the input) so text inside quoted values is never changed. // Longer aliases are tried first so an alias that starts with another alias (e.g. `@@$$` and `@@$`) still matches. private static Parser<(string Operator, bool CaseInsensitive)> ComparisonOperatorAliasParser(IQueryKitConfiguration? config) { diff --git a/README.md b/README.md index 60f7b05..6699e4f 100644 --- a/README.md +++ b/README.md @@ -722,6 +722,8 @@ var filterExpression = FilterParser.ParseFilter(input, config); > `Title @@$ "titilating"` ✅ > `Title@@$"titilating"` ❌ +QueryKit reads a custom operator only where an operator can stand. Operator text inside a quoted value does not change. For example, `Title eq "salt and pepper"` compares `Title` with `salt and pepper`. + #### Allow Unknown Properties By default, QueryKit will throw an error if it doesn't recognize a property name, If you want to loosen the reigns here a bit, you can set `AllowUnknownProperties` to `true` in your config. When active, unknown properties will be ignored in the expression resolution.