Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 56 additions & 0 deletions QueryKit.IntegrationTests/Tests/AliasCultureTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
namespace QueryKit.IntegrationTests.Tests;

using System.Globalization;
using Configuration;
using Exceptions;
using FluentAssertions;
using Microsoft.EntityFrameworkCore;
using SharedTestingHelper.Fakes;
using WebApiTestProject.Entities;

// Like v1.14.2, a query name matches with the case rules of the culture of each parse,
// also when a parse in another culture used the same query name before.
public class AliasCultureTests : TestBase
{
[Fact]
public async Task query_name_matches_in_en_us_after_a_tr_tr_parse()
{
// Arrange
var testingServiceScope = new TestingServiceScope();
var title = $"culture {Guid.NewGuid()}";
var fakePersonOne = new FakeTestingPersonBuilder()
.WithTitle(title)
.Build();
var fakePersonTwo = new FakeTestingPersonBuilder()
.Build();
await testingServiceScope.InsertAsync(fakePersonOne, fakePersonTwo);

var input = $"""ISIMDELTA == "{title}" """;
var config = new QueryKitConfiguration(config =>
{
config.Property<TestingPerson>(x => x.Title).HasQueryName("isimdelta");

Check warning on line 31 in QueryKit.IntegrationTests/Tests/AliasCultureTests.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference return.

Check warning on line 31 in QueryKit.IntegrationTests/Tests/AliasCultureTests.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference return.

Check warning on line 31 in QueryKit.IntegrationTests/Tests/AliasCultureTests.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference return.

Check warning on line 31 in QueryKit.IntegrationTests/Tests/AliasCultureTests.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference return.
});

// Act
var originalCulture = CultureInfo.CurrentCulture;
IQueryable<TestingPerson> appliedQueryable;
try
{
CultureInfo.CurrentCulture = new CultureInfo("tr-TR");
var turkish = () => testingServiceScope.DbContext().People.ApplyQueryKitFilter(input, config);
turkish.Should().Throw<UnknownFilterPropertyException>();

CultureInfo.CurrentCulture = new CultureInfo("en-US");
appliedQueryable = testingServiceScope.DbContext().People.ApplyQueryKitFilter(input, config);
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
}
var people = await appliedQueryable.ToListAsync();

// Assert
people.Count.Should().Be(1);
people[0].Id.Should().Be(fakePersonOne.Id);
}
}
75 changes: 75 additions & 0 deletions QueryKit.UnitTests/AliasCultureTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
namespace QueryKit.UnitTests;

using System.Globalization;
using Configuration;
using Exceptions;
using FluentAssertions;
using WebApiTestProject.Entities.Recipes;

// Like v1.14.2, an alias matches with the case rules of the culture of each parse,
// also when a parse in another culture used the same alias before.
// Each test uses its own alias, so the result does not depend on the order of the tests.
public class AliasCultureTests
{
[Fact]
public void query_name_matches_in_en_us_after_a_tr_tr_parse()
{
var config = new QueryKitConfiguration(settings =>
{
settings.Property<Recipe>(x => x.Rating).HasQueryName("tipalpha");

Check warning on line 19 in QueryKit.UnitTests/AliasCultureTests.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference return.

Check warning on line 19 in QueryKit.UnitTests/AliasCultureTests.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference return.

Check warning on line 19 in QueryKit.UnitTests/AliasCultureTests.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference return.

Check warning on line 19 in QueryKit.UnitTests/AliasCultureTests.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference return.
});
var input = "TIPALPHA > 3";

var turkish = () => WithCulture("tr-TR", () => FilterParser.ParseFilter<Recipe>(input, config));
turkish.Should().Throw<UnknownFilterPropertyException>();

var filterExpression = WithCulture("en-US", () => FilterParser.ParseFilter<Recipe>(input, config));
filterExpression.ToString().Should().Be("x => (x.Rating > 3)");
}

[Fact]
public void query_name_does_not_match_in_tr_tr_after_an_en_us_parse()
{
var config = new QueryKitConfiguration(settings =>
{
settings.Property<Recipe>(x => x.Rating).HasQueryName("tipbeta");

Check warning on line 35 in QueryKit.UnitTests/AliasCultureTests.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference return.

Check warning on line 35 in QueryKit.UnitTests/AliasCultureTests.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference return.

Check warning on line 35 in QueryKit.UnitTests/AliasCultureTests.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference return.

Check warning on line 35 in QueryKit.UnitTests/AliasCultureTests.cs

View workflow job for this annotation

GitHub Actions / test

Possible null reference return.
});
var input = "TIPBETA > 3";

var filterExpression = WithCulture("en-US", () => FilterParser.ParseFilter<Recipe>(input, config));
filterExpression.ToString().Should().Be("x => (x.Rating > 3)");

var turkish = () => WithCulture("tr-TR", () => FilterParser.ParseFilter<Recipe>(input, config));
turkish.Should().Throw<UnknownFilterPropertyException>();
}

[Fact]
public void operator_alias_does_not_match_in_tr_tr_after_an_en_us_parse()
{
var config = new QueryKitConfiguration(settings =>
{
settings.EqualsOperator = "eşitgamma";
});
var input = """Title EŞITGAMMA "Pancakes" """;

var filterExpression = WithCulture("en-US", () => FilterParser.ParseFilter<Recipe>(input, config));
filterExpression.ToString().Should().Be("""x => (x.Title == "Pancakes")""");

var turkish = () => WithCulture("tr-TR", () => FilterParser.ParseFilter<Recipe>(input, config));
turkish.Should().Throw<ParsingException>();
}

private static TResult WithCulture<TResult>(string cultureName, Func<TResult> action)
{
var originalCulture = CultureInfo.CurrentCulture;
try
{
CultureInfo.CurrentCulture = new CultureInfo(cultureName);
return action();
}
finally
{
CultureInfo.CurrentCulture = originalCulture;
}
}
}
9 changes: 6 additions & 3 deletions QueryKit/AliasRegexCache.cs
Original file line number Diff line number Diff line change
@@ -1,15 +1,18 @@
namespace QueryKit;

using System.Collections.Concurrent;
using System.Globalization;
using System.Text.RegularExpressions;

// Keeps one Regex for each alias pattern, so a parse does not build a new Regex for each alias.
// Keeps one Regex for each alias pattern and culture, so a parse does not build a new Regex for each alias.
// The patterns come only from the configuration (query names and operator aliases), never from
// the filter text, so the number of entries stays small.
// RegexOptions.IgnoreCase uses the current culture when the Regex is built, so the key includes the culture.
// Like v1.14.2, which built a new Regex for each parse, each culture keeps its own case rules.
internal static class AliasRegexCache
{
private static readonly ConcurrentDictionary<string, Regex> Cache = new();
private static readonly ConcurrentDictionary<(string Pattern, string Culture), Regex> Cache = new();

public static Regex Get(string pattern)
=> Cache.GetOrAdd(pattern, p => new Regex(p, RegexOptions.IgnoreCase));
=> Cache.GetOrAdd((pattern, CultureInfo.CurrentCulture.Name), key => new Regex(key.Pattern, RegexOptions.IgnoreCase));
}
Loading