From 324fee73b3220f7424a5571e80d4b695413e6d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lu=C3=ADs=20Garc=C3=AAs?= Date: Wed, 24 Apr 2024 11:21:16 +0100 Subject: [PATCH] feat: Support contains operator with many to one multiplicity (#165) * feat: support contains operator with many to one multiplicity * fix: fix codacy issue * fix: fix codacy issue --- .../ConditionExpressionBuilderProvider.cs | 17 +-- ...ainsManyToOneConditionExpressionBuilder.cs | 49 ++++++++ ...tainsOneToOneConditionExpressionBuilder.cs | 8 +- .../ContainsOperatorEvalStrategy.cs | 17 ++- .../Evaluation/OperatorsMetadata.cs | 2 +- .../Rules.Framework.IntegrationTests.csproj | 2 +- .../RulesEngine/RulesEngineTestsBase.cs | 84 +++++++++++-- .../OperatorContainsManyToOneTests.cs | 87 +++++++++++++ .../RulesDeactivateAndActivateTests.cs | 15 +-- .../RulesMatching/RulesInSequenceTests.cs | 27 ++-- .../RulesMatching/RulesUpdateDateEndTests.cs | 15 +-- ...Providers.InMemory.IntegrationTests.csproj | 2 +- ....Providers.MongoDb.IntegrationTests.csproj | 2 +- ...s.Framework.Providers.MongoDb.Tests.csproj | 2 +- ...anyToOneConditionExpressionBuilderTests.cs | 115 ++++++++++++++++++ ...OneToOneConditionExpressionBuilderTests.cs | 18 +-- .../ContainsOperatorEvalStrategyTests.cs | 74 ++++++++--- .../Rules.Framework.Tests.csproj | 2 +- .../Rules.Framework.WebUI.Tests.csproj | 2 +- 19 files changed, 457 insertions(+), 83 deletions(-) create mode 100644 src/Rules.Framework/Evaluation/Compiled/ConditionBuilders/ContainsManyToOneConditionExpressionBuilder.cs create mode 100644 tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/OperatorContainsManyToOneTests.cs create mode 100644 tests/Rules.Framework.Tests/Evaluation/Compiled/ConditionBuilders/ContainsManyToOneConditionExpressionBuilderTests.cs diff --git a/src/Rules.Framework/Evaluation/Compiled/ConditionBuilders/ConditionExpressionBuilderProvider.cs b/src/Rules.Framework/Evaluation/Compiled/ConditionBuilders/ConditionExpressionBuilderProvider.cs index d7578b7b..13ac327d 100644 --- a/src/Rules.Framework/Evaluation/Compiled/ConditionBuilders/ConditionExpressionBuilderProvider.cs +++ b/src/Rules.Framework/Evaluation/Compiled/ConditionBuilders/ConditionExpressionBuilderProvider.cs @@ -12,22 +12,23 @@ public ConditionExpressionBuilderProvider() { this.conditionExpressionBuilders = new Dictionary(StringComparer.Ordinal) { + { Combine(Operators.CaseInsensitiveEndsWith, Multiplicities.OneToOne), new CaseInsensitiveEndsWithOneToOneConditionExpressionBuilder() }, + { Combine(Operators.CaseInsensitiveStartsWith, Multiplicities.OneToOne), new CaseInsensitiveStartsWithOneToOneConditionExpressionBuilder() }, + { Combine(Operators.Contains, Multiplicities.ManyToOne), new ContainsManyToOneConditionExpressionBuilder() }, + { Combine(Operators.Contains, Multiplicities.OneToOne), new ContainsOneToOneConditionExpressionBuilder() }, + { Combine(Operators.EndsWith, Multiplicities.OneToOne), new EndsWithOneToOneConditionExpressionBuilder() }, { Combine(Operators.Equal, Multiplicities.OneToOne), new EqualOneToOneConditionExpressionBuilder() }, - { Combine(Operators.NotEqual, Multiplicities.OneToOne), new NotEqualOneToOneConditionExpressionBuilder() }, { Combine(Operators.GreaterThan, Multiplicities.OneToOne), new GreaterThanOneToOneConditionExpressionBuilder() }, { Combine(Operators.GreaterThanOrEqual, Multiplicities.OneToOne), new GreaterThanOrEqualOneToOneConditionExpressionBuilder() }, + { Combine(Operators.In, Multiplicities.OneToMany), new InOneToManyConditionExpressionBuilder() }, { Combine(Operators.LesserThan, Multiplicities.OneToOne), new LesserThanOneToOneConditionExpressionBuilder() }, { Combine(Operators.LesserThanOrEqual, Multiplicities.OneToOne), new LesserThanOrEqualOneToOneConditionExpressionBuilder() }, - { Combine(Operators.Contains, Multiplicities.OneToOne), new ContainsOneToOneConditionExpressionBuilder() }, { Combine(Operators.NotContains, Multiplicities.OneToOne), new NotContainsOneToOneConditionExpressionBuilder() }, - { Combine(Operators.In, Multiplicities.OneToMany), new InOneToManyConditionExpressionBuilder() }, - { Combine(Operators.StartsWith, Multiplicities.OneToOne), new StartsWithOneToOneConditionExpressionBuilder() }, - { Combine(Operators.EndsWith, Multiplicities.OneToOne), new EndsWithOneToOneConditionExpressionBuilder() }, - { Combine(Operators.CaseInsensitiveStartsWith, Multiplicities.OneToOne), new CaseInsensitiveStartsWithOneToOneConditionExpressionBuilder() }, - { Combine(Operators.CaseInsensitiveEndsWith, Multiplicities.OneToOne), new CaseInsensitiveEndsWithOneToOneConditionExpressionBuilder() }, { Combine(Operators.NotEndsWith, Multiplicities.OneToOne), new NotEndsWithOneToOneConditionExpressionBuilder() }, - { Combine(Operators.NotStartsWith, Multiplicities.OneToOne), new NotStartsWithOneToOneConditionExpressionBuilder() }, + { Combine(Operators.NotEqual, Multiplicities.OneToOne), new NotEqualOneToOneConditionExpressionBuilder() }, { Combine(Operators.NotIn, Multiplicities.OneToMany), new NotInOneToManyConditionExpressionBuilder() }, + { Combine(Operators.NotStartsWith, Multiplicities.OneToOne), new NotStartsWithOneToOneConditionExpressionBuilder() }, + { Combine(Operators.StartsWith, Multiplicities.OneToOne), new StartsWithOneToOneConditionExpressionBuilder() }, }; } diff --git a/src/Rules.Framework/Evaluation/Compiled/ConditionBuilders/ContainsManyToOneConditionExpressionBuilder.cs b/src/Rules.Framework/Evaluation/Compiled/ConditionBuilders/ContainsManyToOneConditionExpressionBuilder.cs new file mode 100644 index 00000000..91f5873f --- /dev/null +++ b/src/Rules.Framework/Evaluation/Compiled/ConditionBuilders/ContainsManyToOneConditionExpressionBuilder.cs @@ -0,0 +1,49 @@ +namespace Rules.Framework.Evaluation.Compiled.ConditionBuilders +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + using Rules.Framework.Core; + using Rules.Framework.Evaluation.Compiled.ExpressionBuilders; + + internal sealed class ContainsManyToOneConditionExpressionBuilder : IConditionExpressionBuilder + { + private static readonly Dictionary containsLinqGenericMethodInfos = InitializeLinqContainsMethodInfos(); + private static readonly DataTypes[] supportedDataTypes = { DataTypes.Boolean, DataTypes.Decimal, DataTypes.Integer, DataTypes.String }; + + public Expression BuildConditionExpression(IExpressionBlockBuilder builder, BuildConditionExpressionArgs args) + { + if (!supportedDataTypes.Contains(args.DataTypeConfiguration.DataType)) + { + throw new NotSupportedException( + $"The operator '{nameof(Operators.Contains)}' is not supported for data type '{args.DataTypeConfiguration.DataType}' on a many to one scenario."); + } + + var containsMethodInfo = containsLinqGenericMethodInfos[args.DataTypeConfiguration.Type]; + + return builder.AndAlso( + builder.NotEqual(args.LeftHandOperand, builder.Constant(value: null!)), + builder.Call( + null!, + containsMethodInfo, + new Expression[] { args.LeftHandOperand, args.RightHandOperand })); + } + + private static Dictionary InitializeLinqContainsMethodInfos() + { + var genericMethodInfo = typeof(Enumerable) + .GetMethods() + .First(m => string.Equals(m.Name, nameof(Enumerable.Contains), StringComparison.Ordinal) && m.GetParameters().Length == 2); + + return new Dictionary + { + { typeof(bool), genericMethodInfo.MakeGenericMethod(typeof(bool)) }, + { typeof(decimal), genericMethodInfo.MakeGenericMethod(typeof(decimal)) }, + { typeof(int), genericMethodInfo.MakeGenericMethod(typeof(int)) }, + { typeof(string), genericMethodInfo.MakeGenericMethod(typeof(string)) }, + }; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework/Evaluation/Compiled/ConditionBuilders/ContainsOneToOneConditionExpressionBuilder.cs b/src/Rules.Framework/Evaluation/Compiled/ConditionBuilders/ContainsOneToOneConditionExpressionBuilder.cs index 4412c992..00cc3b56 100644 --- a/src/Rules.Framework/Evaluation/Compiled/ConditionBuilders/ContainsOneToOneConditionExpressionBuilder.cs +++ b/src/Rules.Framework/Evaluation/Compiled/ConditionBuilders/ContainsOneToOneConditionExpressionBuilder.cs @@ -1,6 +1,7 @@ namespace Rules.Framework.Evaluation.Compiled.ConditionBuilders { using System; + using System.Linq; using System.Linq.Expressions; using System.Reflection; using Rules.Framework.Core; @@ -8,17 +9,18 @@ namespace Rules.Framework.Evaluation.Compiled.ConditionBuilders internal sealed class ContainsOneToOneConditionExpressionBuilder : IConditionExpressionBuilder { - private static readonly MethodInfo stringContainsMethodInfo = typeof(string).GetMethod("Contains", new[] { typeof(string) }); + private static readonly MethodInfo stringContainsMethodInfo = typeof(string).GetMethod(nameof(Enumerable.Contains), new[] { typeof(string) }); public Expression BuildConditionExpression(IExpressionBlockBuilder builder, BuildConditionExpressionArgs args) { if (args.DataTypeConfiguration.DataType != DataTypes.String) { - throw new NotSupportedException($"The operator '{Operators.Contains}' is not supported for data type '{args.DataTypeConfiguration.DataType}'."); + throw new NotSupportedException( + $"The operator '{nameof(Operators.Contains)}' is not supported for data type '{args.DataTypeConfiguration.DataType}' on a one to one scenario."); } return builder.AndAlso( - builder.NotEqual(args.LeftHandOperand, builder.Constant(value: null)), + builder.NotEqual(args.LeftHandOperand, builder.Constant(value: null!)), builder.Call( args.LeftHandOperand, stringContainsMethodInfo, diff --git a/src/Rules.Framework/Evaluation/Interpreted/ValueEvaluation/ContainsOperatorEvalStrategy.cs b/src/Rules.Framework/Evaluation/Interpreted/ValueEvaluation/ContainsOperatorEvalStrategy.cs index 9c890223..f8e85727 100644 --- a/src/Rules.Framework/Evaluation/Interpreted/ValueEvaluation/ContainsOperatorEvalStrategy.cs +++ b/src/Rules.Framework/Evaluation/Interpreted/ValueEvaluation/ContainsOperatorEvalStrategy.cs @@ -1,20 +1,29 @@ namespace Rules.Framework.Evaluation.Interpreted.ValueEvaluation { using System; + using System.Collections.Generic; + using System.Linq; - internal sealed class ContainsOperatorEvalStrategy : IOneToOneOperatorEvalStrategy + internal sealed class ContainsOperatorEvalStrategy : IOneToOneOperatorEvalStrategy, IManyToOneOperatorEvalStrategy { public bool Eval(object leftOperand, object rightOperand) { if (leftOperand is string) { - string leftOperandAsString = leftOperand as string; - string rightOperandAsString = rightOperand as string; + var leftOperandAsString = leftOperand as string; + var rightOperandAsString = rightOperand as string; - return leftOperandAsString.Contains(rightOperandAsString); +#if NETSTANDARD2_1_OR_GREATER + return leftOperandAsString!.Contains(rightOperandAsString, StringComparison.Ordinal); +#else + return leftOperandAsString!.Contains(rightOperandAsString); +#endif } throw new NotSupportedException($"Unsupported 'contains' comparison between operands of type '{leftOperand?.GetType().FullName}'."); } + + public bool Eval(IEnumerable leftOperand, object rightOperand) + => leftOperand.Contains(rightOperand); } } \ No newline at end of file diff --git a/src/Rules.Framework/Evaluation/OperatorsMetadata.cs b/src/Rules.Framework/Evaluation/OperatorsMetadata.cs index 285d2d1a..c11a2216 100644 --- a/src/Rules.Framework/Evaluation/OperatorsMetadata.cs +++ b/src/Rules.Framework/Evaluation/OperatorsMetadata.cs @@ -76,7 +76,7 @@ static OperatorsMetadata() public static OperatorMetadata Contains => new() { Operator = Operators.Contains, - SupportedMultiplicities = new[] { Multiplicities.OneToOne }, + SupportedMultiplicities = new[] { Multiplicities.OneToOne, Multiplicities.ManyToOne }, }; public static OperatorMetadata EndsWith => new() diff --git a/tests/Rules.Framework.IntegrationTests/Rules.Framework.IntegrationTests.csproj b/tests/Rules.Framework.IntegrationTests/Rules.Framework.IntegrationTests.csproj index a7253dda..2e117793 100644 --- a/tests/Rules.Framework.IntegrationTests/Rules.Framework.IntegrationTests.csproj +++ b/tests/Rules.Framework.IntegrationTests/Rules.Framework.IntegrationTests.csproj @@ -23,7 +23,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesEngineTestsBase.cs b/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesEngineTestsBase.cs index 2bc6d550..642a55fd 100644 --- a/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesEngineTestsBase.cs +++ b/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesEngineTestsBase.cs @@ -2,6 +2,7 @@ namespace Rules.Framework.Providers.InMemory.IntegrationTests.Features.RulesEngi { using System; using System.Collections.Generic; + using System.Globalization; using System.Threading.Tasks; using Rules.Framework.Core; using Rules.Framework.IntegrationTests.Common.Features; @@ -15,7 +16,19 @@ protected RulesEngineTestsBase(ContentType testContentType) { this.TestContentType = testContentType; - this.RulesEngine = RulesEngineBuilder + this.CompiledRulesEngine = RulesEngineBuilder + .CreateRulesEngine() + .WithContentType() + .WithConditionType() + .SetInMemoryDataSource() + .Configure(c => + { + c.EnableCompilation = true; + c.PriorityCriteria = PriorityCriterias.TopmostRuleWins; + }) + .Build(); + + this.InterpretedRulesEngine = RulesEngineBuilder .CreateRulesEngine() .WithContentType() .WithConditionType() @@ -24,27 +37,76 @@ protected RulesEngineTestsBase(ContentType testContentType) .Build(); } - protected RulesEngine RulesEngine { get; } + protected RulesEngine CompiledRulesEngine { get; } + + protected RulesEngine InterpretedRulesEngine { get; } + + protected async Task ActivateRuleAsync(Rule rule, bool compiled) + { + if (compiled) + { + return await CompiledRulesEngine.ActivateRuleAsync(rule); + } + else + { + return await InterpretedRulesEngine.ActivateRuleAsync(rule); + } + } protected void AddRules(IEnumerable ruleSpecifications) { foreach (var ruleSpecification in ruleSpecifications) { - this.RulesEngine.AddRuleAsync( - ruleSpecification.Rule, - ruleSpecification.RuleAddPriorityOption) - .ConfigureAwait(false) + this.CompiledRulesEngine.AddRuleAsync(ruleSpecification.Rule, ruleSpecification.RuleAddPriorityOption) .GetAwaiter() .GetResult(); + + this.InterpretedRulesEngine.AddRuleAsync(ruleSpecification.Rule, ruleSpecification.RuleAddPriorityOption) + .GetAwaiter() + .GetResult(); + } + } + + protected async Task DeactivateRuleAsync(Rule rule, bool compiled) + { + if (compiled) + { + return await CompiledRulesEngine.DeactivateRuleAsync(rule); + } + else + { + return await InterpretedRulesEngine.DeactivateRuleAsync(rule); } } protected async Task> MatchOneAsync( DateTime matchDate, - Condition[] conditions) => await RulesEngine.MatchOneAsync( - TestContentType, - matchDate, - conditions) - .ConfigureAwait(false); + Condition[] conditions, + bool compiled) + { + if (compiled) + { + return await CompiledRulesEngine.MatchOneAsync(TestContentType, matchDate, conditions); + } + else + { + return await InterpretedRulesEngine.MatchOneAsync(TestContentType, matchDate, conditions); + } + } + + protected async Task UpdateRuleAsync(Rule rule, bool compiled) + { + if (compiled) + { + return await CompiledRulesEngine.UpdateRuleAsync(rule); + } + else + { + return await InterpretedRulesEngine.UpdateRuleAsync(rule); + } + } + + protected DateTime UtcDate(string date) + => DateTime.Parse(date, CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal); } } \ No newline at end of file diff --git a/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/OperatorContainsManyToOneTests.cs b/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/OperatorContainsManyToOneTests.cs new file mode 100644 index 00000000..bf846d19 --- /dev/null +++ b/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/OperatorContainsManyToOneTests.cs @@ -0,0 +1,87 @@ +namespace Rules.Framework.Providers.InMemory.IntegrationTests.Features.RulesEngine.RulesMatching +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using FluentAssertions; + using Rules.Framework.Core; + using Rules.Framework.IntegrationTests.Common.Features; + using Rules.Framework.Tests.Stubs; + using Xunit; + + public class OperatorContainsManyToOneTests : RulesEngineTestsBase + { + private static readonly ContentType testContentType = ContentType.ContentType1; + private readonly Rule expectedMatchRule; + private readonly Rule otherRule; + + public OperatorContainsManyToOneTests() + : base(testContentType) + { + this.expectedMatchRule = RuleBuilder.NewRule() + .WithName("Expected rule") + .WithDateBegin(UtcDate("2020-01-01Z")) + .WithContent(testContentType, "Just as expected!") + .WithCondition(ConditionType.ConditionType1, Operators.Contains, "Cat") + .Build() + .Rule; + + this.otherRule = RuleBuilder.NewRule() + .WithName("Other rule") + .WithDateBegin(UtcDate("2020-01-01Z")) + .WithContent(testContentType, "Oops! Not expected to be matched.") + .Build() + .Rule; + + this.AddRules(this.CreateTestRules()); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task RulesEngine_GivenConditionType1WithArrayOfStringsContainingCat_MatchesExpectedRule(bool compiled) + { + // Arrange + var emptyConditions = new[] + { + new Condition(ConditionType.ConditionType1, new[]{ "Dog", "Fish", "Cat", "Spider", "Mockingbird", }) + }; + var matchDate = UtcDate("2020-01-02Z"); + + // Act + var actualMatch = await this.MatchOneAsync(matchDate, emptyConditions, compiled); + + // Assert + actualMatch.Should().BeEquivalentTo(expectedMatchRule); + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task RulesEngine_GivenConditionType1WithArrayOfStringsNotContainingCat_MatchesOtherRule(bool compiled) + { + // Arrange + var emptyConditions = new[] + { + new Condition(ConditionType.ConditionType1, new[]{ "Dog", "Fish", "Bat", "Spider", "Mockingbird", }) + }; + var matchDate = UtcDate("2020-01-02Z"); + + // Act + var actualMatch = await this.MatchOneAsync(matchDate, emptyConditions, compiled); + + // Assert + actualMatch.Should().BeEquivalentTo(otherRule); + } + + private IEnumerable CreateTestRules() + { + var ruleSpecs = new List + { + new RuleSpecification(expectedMatchRule, RuleAddPriorityOption.ByPriorityNumber(1)), + new RuleSpecification(otherRule, RuleAddPriorityOption.ByPriorityNumber(2)) + }; + + return ruleSpecs; + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/RulesDeactivateAndActivateTests.cs b/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/RulesDeactivateAndActivateTests.cs index d6b67509..5771bd7b 100644 --- a/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/RulesDeactivateAndActivateTests.cs +++ b/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/RulesDeactivateAndActivateTests.cs @@ -3,7 +3,6 @@ namespace Rules.Framework.Providers.InMemory.IntegrationTests.Features.RulesEngi using System; using System.Collections.Generic; using System.Threading.Tasks; - using Rules.Framework.Builder; using Rules.Framework.Core; using Rules.Framework.IntegrationTests.Common.Features; using Rules.Framework.Providers.InMemory.IntegrationTests.Features.RulesEngine; @@ -42,28 +41,30 @@ public RulesDeactivateAndActivateTests() : base(TestContentType) this.AddRules(this.CreateTestRules()); } - [Fact] - public async Task RulesEngine_DeactivateThenActivateRule_Validations() + [Theory] + [InlineData(false)] + [InlineData(true)] + public async Task RulesEngine_DeactivateThenActivateRule_Validations(bool compiled) { // Arrange var emptyConditions = Array.Empty>(); var matchDate = new DateTime(2020, 01, 02); // Act 1: Deactivate the rule - var deactivateResult = await this.RulesEngine.DeactivateRuleAsync(rule1); + var deactivateResult = await this.DeactivateRuleAsync(rule1, compiled); // Assert 1: Rule 2 must be found Assert.True(deactivateResult.IsSuccess); - var actualMatch1 = await this.MatchOneAsync(matchDate, emptyConditions).ConfigureAwait(false); + var actualMatch1 = await this.MatchOneAsync(matchDate, emptyConditions, compiled); Assert.NotNull(actualMatch1); Assert.Equal(rule2.Name, actualMatch1.Name); // Act 2: Activate the rule - var activateResult = await this.RulesEngine.ActivateRuleAsync(rule1); + var activateResult = await this.ActivateRuleAsync(rule1, compiled); // Assert 2: Rule 1 must be found Assert.True(activateResult.IsSuccess); - var actualMatch2 = await this.MatchOneAsync(matchDate, emptyConditions).ConfigureAwait(false); + var actualMatch2 = await this.MatchOneAsync(matchDate, emptyConditions, compiled); Assert.NotNull(actualMatch2); Assert.Equal(rule1.Name, actualMatch2.Name); } diff --git a/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/RulesInSequenceTests.cs b/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/RulesInSequenceTests.cs index 63a84d4f..7b411fb3 100644 --- a/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/RulesInSequenceTests.cs +++ b/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/RulesInSequenceTests.cs @@ -3,7 +3,6 @@ namespace Rules.Framework.Providers.InMemory.IntegrationTests.Features.RulesEngi using System; using System.Collections.Generic; using System.Threading.Tasks; - using Rules.Framework.Builder; using Rules.Framework.IntegrationTests.Common.Features; using Rules.Framework.Providers.InMemory.IntegrationTests.Features.RulesEngine; using Rules.Framework.Tests.Stubs; @@ -28,28 +27,34 @@ public RulesInSequenceTests() : base(TestContentType) public static IEnumerable FailureCases => new List { - new object[] { rule1StartDate.AddMilliseconds(-1) }, // before 1st rule - new object[] { rule2EndDate }, // at rules end + new object[] { rule1StartDate.AddMilliseconds(-1), false, }, // before 1st rule + new object[] { rule1StartDate.AddMilliseconds(-1), true, }, // before 1st rule + new object[] { rule2EndDate, false, }, // at rules end + new object[] { rule2EndDate, true, }, // at rules end }; public static IEnumerable SuccessCases => new List { - new object[] { rule1StartDate, rule1Name, rule1Value }, // 1st rule - new object[] { ruleChangeDate.AddMilliseconds(-1), rule1Name, rule1Value }, // immediatly before change - new object[] { ruleChangeDate, rule2Name, rule2Value }, // 2nd rule - new object[] { rule2EndDate.AddMilliseconds(-1), rule2Name, rule2Value }, // immediatly before rules end + new object[] { rule1StartDate, rule1Name, rule1Value, false }, // 1st rule + new object[] { rule1StartDate, rule1Name, rule1Value, true }, // 1st rule + new object[] { ruleChangeDate.AddMilliseconds(-1), rule1Name, rule1Value, false }, // immediatly before change + new object[] { ruleChangeDate.AddMilliseconds(-1), rule1Name, rule1Value, true }, // immediatly before change + new object[] { ruleChangeDate, rule2Name, rule2Value, false }, // 2nd rule + new object[] { ruleChangeDate, rule2Name, rule2Value, true }, // 2nd rule + new object[] { rule2EndDate.AddMilliseconds(-1), rule2Name, rule2Value, false }, // immediatly before rules end + new object[] { rule2EndDate.AddMilliseconds(-1), rule2Name, rule2Value, true }, // immediatly before rules end }; [Theory] [MemberData(nameof(FailureCases))] - public async Task RulesEngine_MatchOneAsync_OutsideRulesPeriod_Failure(DateTime matchDate) + public async Task RulesEngine_MatchOneAsync_OutsideRulesPeriod_Failure(DateTime matchDate, bool compiled) { // Arrange var emptyConditions = Array.Empty>(); // Act - var actualMatch = await this.MatchOneAsync(matchDate, emptyConditions).ConfigureAwait(false); + var actualMatch = await this.MatchOneAsync(matchDate, emptyConditions, compiled); // Assert Assert.Null(actualMatch); @@ -57,13 +62,13 @@ public async Task RulesEngine_MatchOneAsync_OutsideRulesPeriod_Failure(DateTime [Theory] [MemberData(nameof(SuccessCases))] - public async Task RulesEngine_MatchOneAsync_WithRulesInSequence_ReturnsCorrectRule(DateTime matchDate, string expectedName, string expectedValue) + public async Task RulesEngine_MatchOneAsync_WithRulesInSequence_ReturnsCorrectRule(DateTime matchDate, string expectedName, string expectedValue, bool compiled) { // Arrange var emptyConditions = Array.Empty>(); // Act - var actualMatch = await this.MatchOneAsync(matchDate, emptyConditions).ConfigureAwait(false); + var actualMatch = await this.MatchOneAsync(matchDate, emptyConditions, compiled); // Assert Assert.Equal(expectedName, actualMatch.Name); diff --git a/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/RulesUpdateDateEndTests.cs b/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/RulesUpdateDateEndTests.cs index 508580a3..a265a8aa 100644 --- a/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/RulesUpdateDateEndTests.cs +++ b/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Features/RulesEngine/RulesMatching/RulesUpdateDateEndTests.cs @@ -3,7 +3,6 @@ namespace Rules.Framework.Providers.InMemory.IntegrationTests.Features.RulesEngi using System; using System.Collections.Generic; using System.Threading.Tasks; - using Rules.Framework.Builder; using Rules.Framework.Core; using Rules.Framework.IntegrationTests.Common.Features; using Rules.Framework.Providers.InMemory.IntegrationTests.Features.RulesEngine; @@ -45,13 +44,15 @@ public RulesUpdateDateEndTests() : base(TestContentType) public static IEnumerable Cases => new List { - new object[] { new DateTime(2020, 01, 01), true }, - new object[] { new DateTime(2020, 01, 01).AddMilliseconds(-1), false }, + new object[] { new DateTime(2020, 01, 01), true, false }, + new object[] { new DateTime(2020, 01, 01), true, true }, + new object[] { new DateTime(2020, 01, 01).AddMilliseconds(-1), false, false }, + new object[] { new DateTime(2020, 01, 01).AddMilliseconds(-1), false, true }, }; [Theory] [MemberData(nameof(Cases))] - public async Task RulesEngine_UpdateRuleDateEnd_Validations(DateTime dateEnd, bool success) + public async Task RulesEngine_UpdateRuleDateEnd_Validations(DateTime dateEnd, bool success, bool compiled) { // Arrange var emptyConditions = Array.Empty>(); @@ -59,13 +60,13 @@ public async Task RulesEngine_UpdateRuleDateEnd_Validations(DateTime dateEnd, bo // Act rule1.DateEnd = dateEnd; - var updateResult = await this.RulesEngine.UpdateRuleAsync(rule1); + var updateResult = await this.UpdateRuleAsync(rule1, compiled); // Assert Assert.Equal(success, updateResult.IsSuccess); if (success) { - var actualMatch = await this.MatchOneAsync(matchDate, emptyConditions).ConfigureAwait(false); + var actualMatch = await this.MatchOneAsync(matchDate, emptyConditions, compiled); Assert.NotNull(actualMatch); Assert.Equal(rule2.Name, actualMatch.Name); } @@ -82,4 +83,4 @@ private IEnumerable CreateTestRules() return ruleSpecs; } } -} +} \ No newline at end of file diff --git a/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Rules.Framework.Providers.InMemory.IntegrationTests.csproj b/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Rules.Framework.Providers.InMemory.IntegrationTests.csproj index fd2aa7b9..324a3f54 100644 --- a/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Rules.Framework.Providers.InMemory.IntegrationTests.csproj +++ b/tests/Rules.Framework.Providers.InMemory.IntegrationTests/Rules.Framework.Providers.InMemory.IntegrationTests.csproj @@ -10,7 +10,7 @@ - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/Rules.Framework.Providers.MongoDb.IntegrationTests/Rules.Framework.Providers.MongoDb.IntegrationTests.csproj b/tests/Rules.Framework.Providers.MongoDb.IntegrationTests/Rules.Framework.Providers.MongoDb.IntegrationTests.csproj index 4289f4a4..de3197b8 100644 --- a/tests/Rules.Framework.Providers.MongoDb.IntegrationTests/Rules.Framework.Providers.MongoDb.IntegrationTests.csproj +++ b/tests/Rules.Framework.Providers.MongoDb.IntegrationTests/Rules.Framework.Providers.MongoDb.IntegrationTests.csproj @@ -30,7 +30,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Rules.Framework.Providers.MongoDb.Tests/Rules.Framework.Providers.MongoDb.Tests.csproj b/tests/Rules.Framework.Providers.MongoDb.Tests/Rules.Framework.Providers.MongoDb.Tests.csproj index fd60ef77..bb2235d3 100644 --- a/tests/Rules.Framework.Providers.MongoDb.Tests/Rules.Framework.Providers.MongoDb.Tests.csproj +++ b/tests/Rules.Framework.Providers.MongoDb.Tests/Rules.Framework.Providers.MongoDb.Tests.csproj @@ -25,7 +25,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Rules.Framework.Tests/Evaluation/Compiled/ConditionBuilders/ContainsManyToOneConditionExpressionBuilderTests.cs b/tests/Rules.Framework.Tests/Evaluation/Compiled/ConditionBuilders/ContainsManyToOneConditionExpressionBuilderTests.cs new file mode 100644 index 00000000..8c6f178e --- /dev/null +++ b/tests/Rules.Framework.Tests/Evaluation/Compiled/ConditionBuilders/ContainsManyToOneConditionExpressionBuilderTests.cs @@ -0,0 +1,115 @@ +namespace Rules.Framework.Tests.Evaluation.Compiled.ConditionBuilders +{ + using System; + using System.Collections.Generic; + using System.Linq.Expressions; + using FluentAssertions; + using Moq; + using Rules.Framework.Core; + using Rules.Framework.Evaluation; + using Rules.Framework.Evaluation.Compiled.ConditionBuilders; + using Rules.Framework.Evaluation.Compiled.ExpressionBuilders; + using Xunit; + + public class ContainsManyToOneConditionExpressionBuilderTests + { + private readonly ContainsManyToOneConditionExpressionBuilder conditionExpressionBuilder; + + public ContainsManyToOneConditionExpressionBuilderTests() + { + this.conditionExpressionBuilder = new ContainsManyToOneConditionExpressionBuilder(); + } + + public static IEnumerable NotSupportedExceptionCases => new[] + { + new object[] { DataTypes.ArrayBoolean, typeof(IEnumerable), new[] { true, false } }, + new object[] { DataTypes.ArrayDecimal, typeof(IEnumerable), new[] { 1.1m, 2.6m } }, + new object[] { DataTypes.ArrayInteger, typeof(IEnumerable), new[] { 1, 2 } }, + new object[] { DataTypes.ArrayString, typeof(IEnumerable), new[] { "A", "B" } }, + }; + + public static IEnumerable ValidCases => new[] + { + new object[]{ DataTypes.Boolean, typeof(IEnumerable), typeof(bool), new[] { true, }, new[] { false, }, true }, + new object[]{ DataTypes.Decimal, typeof(IEnumerable), typeof(decimal), new[] { 10.5m, 3.6m, 1.9m, }, new[] { 2.4m, 5.6m, 7.0m, }, 10.5m }, + new object[]{ DataTypes.Integer, typeof(IEnumerable), typeof(int), new[] { 1, 2, 3, 4, 5, }, new[] { 10, 11, 12, 13, 14, }, 3 }, + new object[]{ DataTypes.String, typeof(IEnumerable), typeof(string), new[] { "A", "B", "C", "D", }, new[] { "E", "F", "G", "H", }, "C" }, + }; + + [Theory] + [MemberData(nameof(NotSupportedExceptionCases))] + public void BuildConditionExpression_GivenLeftExpressionRightExpressionAndDataTypeConfigurationForInt_ReturnsConditionExpression( + DataTypes dataType, + Type type, + object leftOperand) + { + // Arrange + var args = new BuildConditionExpressionArgs + { + DataTypeConfiguration = DataTypeConfiguration.Create(dataType, type, null), + LeftHandOperand = Expression.Constant(leftOperand), + RightHandOperand = Expression.Constant(2), + }; + + var builder = Mock.Of(); + + // Act + var notSupportedException = Assert.Throws(() => this.conditionExpressionBuilder + .BuildConditionExpression(builder, args)); + + // Assert + notSupportedException.Should().NotBeNull(); + notSupportedException.Message + .Should() + .Contain(Operators.Contains.ToString()) + .And + .Contain(dataType.ToString()); + } + + [Theory] + [MemberData(nameof(ValidCases))] + public void BuildConditionExpression_GivenLeftExpressionRightExpressionAndDataTypeConfigurationForString_ReturnsConditionExpression( + DataTypes dataType, + Type leftOperandType, + Type rightOperandType, + object leftOperandMatch, + object leftOperandNonMatch, + object rightOperand) + { + // Act + var expressionResult = ExpressionBuilder.NewExpression("TestCondition") + .WithParameters(p => + { + p.CreateParameter("leftHand", typeof(object)); + }) + .HavingReturn(typeof(bool), false) + .SetImplementation(builder => + { + var args = new BuildConditionExpressionArgs + { + DataTypeConfiguration = DataTypeConfiguration.Create(dataType, rightOperandType, null), + LeftHandOperand = builder.ConvertChecked(builder.GetParameter("leftHand"), leftOperandType), + RightHandOperand = builder.ConvertChecked(builder.Constant(rightOperand), rightOperandType), + }; + var conditionExpression = this.conditionExpressionBuilder + .BuildConditionExpression(builder, args); + + builder.Return(conditionExpression); + }) + .Build(); + + // Assert + var actualExpression = expressionResult.Implementation; + actualExpression.Should().NotBeNull(); + + var compiledExpression = Expression.Lambda>(actualExpression, expressionResult.Parameters).Compile(true); + var notNullLeftHandValueResult1 = compiledExpression.Invoke(leftOperandMatch); + var notNullLeftHandValueResult2 = compiledExpression.Invoke(leftOperandNonMatch); + var nullLeftHandValueResult = compiledExpression.Invoke(null); + + notNullLeftHandValueResult1.Should().BeTrue(); + notNullLeftHandValueResult2.Should().BeFalse(); + nullLeftHandValueResult.Should().BeFalse(); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Tests/Evaluation/Compiled/ConditionBuilders/ContainsOneToOneConditionExpressionBuilderTests.cs b/tests/Rules.Framework.Tests/Evaluation/Compiled/ConditionBuilders/ContainsOneToOneConditionExpressionBuilderTests.cs index feca11cd..6ac094cf 100644 --- a/tests/Rules.Framework.Tests/Evaluation/Compiled/ConditionBuilders/ContainsOneToOneConditionExpressionBuilderTests.cs +++ b/tests/Rules.Framework.Tests/Evaluation/Compiled/ConditionBuilders/ContainsOneToOneConditionExpressionBuilderTests.cs @@ -12,6 +12,13 @@ namespace Rules.Framework.Tests.Evaluation.Compiled.ConditionBuilders public class ContainsOneToOneConditionExpressionBuilderTests { + private readonly ContainsOneToOneConditionExpressionBuilder containsOneToOneConditionExpressionBuilder; + + public ContainsOneToOneConditionExpressionBuilderTests() + { + this.containsOneToOneConditionExpressionBuilder = new ContainsOneToOneConditionExpressionBuilder(); + } + [Fact] public void BuildConditionExpression_GivenLeftExpressionRightExpressionAndDataTypeConfigurationForInt_ReturnsConditionExpression() { @@ -25,11 +32,8 @@ public void BuildConditionExpression_GivenLeftExpressionRightExpressionAndDataTy var builder = Mock.Of(); - var containsOneToOneConditionExpressionBuilder - = new ContainsOneToOneConditionExpressionBuilder(); - // Act - var notSupportedException = Assert.Throws(() => containsOneToOneConditionExpressionBuilder + var notSupportedException = Assert.Throws(() => this.containsOneToOneConditionExpressionBuilder .BuildConditionExpression(builder, args)); // Assert @@ -44,10 +48,6 @@ var containsOneToOneConditionExpressionBuilder [Fact] public void BuildConditionExpression_GivenLeftExpressionRightExpressionAndDataTypeConfigurationForString_ReturnsConditionExpression() { - // Arrange - var containsOneToOneConditionExpressionBuilder - = new ContainsOneToOneConditionExpressionBuilder(); - // Act var expressionResult = ExpressionBuilder.NewExpression("TestCondition") .WithParameters(p => @@ -63,7 +63,7 @@ var containsOneToOneConditionExpressionBuilder LeftHandOperand = builder.GetParameter("leftHand"), RightHandOperand = builder.Constant("quick"), }; - var conditionExpression = containsOneToOneConditionExpressionBuilder + var conditionExpression = this.containsOneToOneConditionExpressionBuilder .BuildConditionExpression(builder, args); builder.Return(conditionExpression); diff --git a/tests/Rules.Framework.Tests/Evaluation/Interpreted/ValueEvaluation/ContainsOperatorEvalStrategyTests.cs b/tests/Rules.Framework.Tests/Evaluation/Interpreted/ValueEvaluation/ContainsOperatorEvalStrategyTests.cs index 1e349728..0684c7cf 100644 --- a/tests/Rules.Framework.Tests/Evaluation/Interpreted/ValueEvaluation/ContainsOperatorEvalStrategyTests.cs +++ b/tests/Rules.Framework.Tests/Evaluation/Interpreted/ValueEvaluation/ContainsOperatorEvalStrategyTests.cs @@ -1,23 +1,69 @@ namespace Rules.Framework.Tests.Evaluation.Interpreted.ValueEvaluation { + using System; + using System.Collections; + using System.Collections.Generic; + using System.Linq; using FluentAssertions; using Rules.Framework.Evaluation.Interpreted.ValueEvaluation; - using System; using Xunit; public class ContainsOperatorEvalStrategyTests { + private readonly ContainsOperatorEvalStrategy operatorEvalStrategy; + + public ContainsOperatorEvalStrategyTests() + { + this.operatorEvalStrategy = new ContainsOperatorEvalStrategy(); + } + + public static IEnumerable ArrayLeftOperandFailureCases => new[] + { + new object[] { new[] { true, }, false }, + new object[] { new[] { 6.5m, 7.1m, 8.6m, }, 8.1m }, + new object[] { new[] { 1, 2, 3, 4, 5, }, 10 }, + new object[] { new[] { "C", "F", "M", "Z", }, "A" }, + }; + + public static IEnumerable ArrayLeftOperandSuccessCases => new[] + { + new object[] { new[] { true, }, true }, + new object[] { new[] { 6.5m, 7.1m, 8.6m, }, 6.5m }, + new object[] { new[] { 1, 2, 3, 4, 5, }, 4 }, + new object[] { new[] { "C", "F", "M", "Z", }, "M" }, + }; + + [Theory] + [MemberData(nameof(ArrayLeftOperandFailureCases))] + public void Eval_GivenArrayOfTypeAndType_ReturnsFalse(IEnumerable expectedLeftOperand, object expectedRightOperand) + { + // Act + var actual = this.operatorEvalStrategy.Eval(expectedLeftOperand.Cast(), expectedRightOperand); + + // Arrange + actual.Should().BeFalse(); + } + + [Theory] + [MemberData(nameof(ArrayLeftOperandSuccessCases))] + public void Eval_GivenArrayOfTypeAndType_ReturnsTrue(IEnumerable expectedLeftOperand, object expectedRightOperand) + { + // Act + var actual = this.operatorEvalStrategy.Eval(expectedLeftOperand.Cast(), expectedRightOperand); + + // Arrange + actual.Should().BeTrue(); + } + [Fact] public void Eval_GivenIntegers1And2_ThrowsNotSupportedException() { // Arrange - int expectedLeftOperand = 1; - int expectedRightOperand = 2; - - ContainsOperatorEvalStrategy sut = new ContainsOperatorEvalStrategy(); + var expectedLeftOperand = 1; + var expectedRightOperand = 2; // Act - NotSupportedException notSupportedException = Assert.Throws(() => sut.Eval(expectedLeftOperand, expectedRightOperand)); + var notSupportedException = Assert.Throws(() => this.operatorEvalStrategy.Eval(expectedLeftOperand, expectedRightOperand)); // Arrange notSupportedException.Should().NotBeNull(); @@ -28,13 +74,11 @@ public void Eval_GivenIntegers1And2_ThrowsNotSupportedException() public void Eval_GivenStringsTheQuickBrownFoxJumpsOverTheLazyDogAndFox_ReturnsTrue() { // Arrange - string expectedLeftOperand = "The quick brown fox jumps over the lazy dog"; - string expectedRightOperand = "fox"; - - ContainsOperatorEvalStrategy sut = new ContainsOperatorEvalStrategy(); + var expectedLeftOperand = "The quick brown fox jumps over the lazy dog"; + var expectedRightOperand = "fox"; // Act - bool actual = sut.Eval(expectedLeftOperand, expectedRightOperand); + var actual = this.operatorEvalStrategy.Eval(expectedLeftOperand, expectedRightOperand); // Arrange actual.Should().BeTrue(); @@ -44,13 +88,11 @@ public void Eval_GivenStringsTheQuickBrownFoxJumpsOverTheLazyDogAndFox_ReturnsTr public void Eval_GivenStringsTheQuickBrownFoxJumpsOverTheLazyDogAndYellow_ReturnsFalse() { // Arrange - string expectedLeftOperand = "The quick brown fox jumps over the lazy dog"; - string expectedRightOperand = "yellow"; - - ContainsOperatorEvalStrategy sut = new ContainsOperatorEvalStrategy(); + var expectedLeftOperand = "The quick brown fox jumps over the lazy dog"; + var expectedRightOperand = "yellow"; // Act - bool actual = sut.Eval(expectedLeftOperand, expectedRightOperand); + var actual = this.operatorEvalStrategy.Eval(expectedLeftOperand, expectedRightOperand); // Arrange actual.Should().BeFalse(); diff --git a/tests/Rules.Framework.Tests/Rules.Framework.Tests.csproj b/tests/Rules.Framework.Tests/Rules.Framework.Tests.csproj index 333cee17..b35bf875 100644 --- a/tests/Rules.Framework.Tests/Rules.Framework.Tests.csproj +++ b/tests/Rules.Framework.Tests/Rules.Framework.Tests.csproj @@ -45,7 +45,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/Rules.Framework.WebUI.Tests/Rules.Framework.WebUI.Tests.csproj b/tests/Rules.Framework.WebUI.Tests/Rules.Framework.WebUI.Tests.csproj index f59f3438..407f146e 100644 --- a/tests/Rules.Framework.WebUI.Tests/Rules.Framework.WebUI.Tests.csproj +++ b/tests/Rules.Framework.WebUI.Tests/Rules.Framework.WebUI.Tests.csproj @@ -20,7 +20,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive