From a2e0d97e6b51305ef018a65e26489db724b40f3e Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Sat, 18 May 2024 15:46:56 +0100 Subject: [PATCH 1/9] feat: create RQL and support match/search expressions --- rules-framework.sln | 28 + src/Rules.Framework.Rql/AssemblyMetadata.cs | 5 + .../Ast/Expressions/AssignmentExpression.cs | 26 + .../Ast/Expressions/BinaryExpression.cs | 25 + .../Ast/Expressions/Expression.cs | 24 + .../Ast/Expressions/IExpressionVisitor.cs | 29 + .../Ast/Expressions/IdentifierExpression.cs | 19 + .../Ast/Expressions/KeywordExpression.cs | 22 + .../Ast/Expressions/LiteralExpression.cs | 28 + .../Ast/Expressions/LiteralType.cs | 12 + .../Ast/Expressions/MatchExpression.cs | 38 + .../Ast/Expressions/NewArrayExpression.cs | 34 + .../Ast/Expressions/NewObjectExpression.cs | 23 + .../Ast/Expressions/NoneExpression.cs | 15 + .../Ast/Expressions/PlaceholderExpression.cs | 19 + .../Ast/Expressions/SearchExpression.cs | 31 + .../Ast/Expressions/UnaryExpression.cs | 22 + src/Rules.Framework.Rql/Ast/IAstElement.cs | 13 + .../Ast/Segments/CardinalitySegment.cs | 25 + .../Ast/Segments/ISegmentVisitor.cs | 15 + .../Ast/Segments/InputConditionSegment.cs | 27 + .../Ast/Segments/InputConditionsSegment.cs | 19 + .../Ast/Segments/NoneSegment.cs | 15 + .../Ast/Segments/OperatorSegment.cs | 19 + .../Ast/Segments/Segment.cs | 23 + .../Ast/Statements/ExpressionStatement.cs | 22 + .../Ast/Statements/IStatementVisitor.cs | 9 + .../Ast/Statements/NoneStatement.cs | 15 + .../Ast/Statements/Statement.cs | 23 + src/Rules.Framework.Rql/IResult.cs | 7 + src/Rules.Framework.Rql/IReverseRqlBuilder.cs | 9 + src/Rules.Framework.Rql/IRqlEngine.cs | 11 + .../Messages/IMessageContainer.cs | 11 + src/Rules.Framework.Rql/Messages/Message.cs | 27 + .../Messages/MessageContainer.cs | 68 ++ .../Messages/MessageSeverity.cs | 9 + src/Rules.Framework.Rql/NothingResult.cs | 15 + .../Interpret/ErrorStatementResult.cs | 28 + .../Interpret/ExpressionStatementResult.cs | 22 + .../Pipeline/Interpret/IInterpreter.cs | 11 + .../Pipeline/Interpret/IResult.cs | 11 + .../Pipeline/Interpret/InterpretResult.cs | 32 + .../Pipeline/Interpret/Interpreter.cs | 371 ++++++++++ .../Interpret/InterpreterException.cs | 36 + .../Interpret/NothingStatementResult.cs | 24 + .../Parse/IExpressionParseStrategy.cs | 8 + .../Pipeline/Parse/IParseStrategy.cs | 7 + .../Pipeline/Parse/IParseStrategyProvider.cs | 11 + .../Pipeline/Parse/IParser.cs | 10 + .../Pipeline/Parse/ISegmentParseStrategy.cs | 8 + .../Pipeline/Parse/IStatementParseStrategy.cs | 8 + .../Pipeline/Parse/PanicModeInfo.cs | 21 + .../Pipeline/Parse/ParseContext.cs | 146 ++++ .../Pipeline/Parse/ParseResult.cs | 28 + .../Pipeline/Parse/ParseStrategyPool.cs | 55 ++ .../Pipeline/Parse/Parser.cs | 68 ++ .../Parse/Strategies/ArrayParseStrategy.cs | 106 +++ .../Strategies/AssignmentParseStrategy.cs | 20 + .../Strategies/BaseExpressionParseStrategy.cs | 46 ++ .../Strategies/CardinalityParseStrategy.cs | 47 ++ .../Strategies/ContentTypeParseStrategy.cs | 47 ++ .../Strategies/DeclarationParseStrategy.cs | 20 + .../Strategies/ExpressionParseStrategy.cs | 17 + .../ExpressionStatementParseStrategy.cs | 30 + .../Parse/Strategies/FactorParseStrategy.cs | 42 ++ .../Strategies/IdentifierParseStrategy.cs | 18 + .../Strategies/InputConditionParseStrategy.cs | 48 ++ .../InputConditionsParseStrategy.cs | 66 ++ .../Parse/Strategies/KeywordParseStrategy.cs | 18 + .../Parse/Strategies/LiteralParseStrategy.cs | 41 ++ .../Strategies/MatchRulesParseStrategy.cs | 100 +++ .../Parse/Strategies/NothingParseStrategy.cs | 24 + .../Parse/Strategies/ObjectParseStrategy.cs | 89 +++ .../Parse/Strategies/OperatorParseStrategy.cs | 58 ++ .../Parse/Strategies/ParseStrategyBase.cs | 27 + .../RulesManipulationParseStrategy.cs | 30 + .../Strategies/SearchRulesParseStrategy.cs | 112 +++ .../Strategies/StatementParseStrategy.cs | 20 + .../Parse/Strategies/TermParseStrategy.cs | 41 ++ .../Parse/Strategies/UnaryParseStrategy.cs | 31 + .../Pipeline/Scan/IScanner.cs | 7 + .../Pipeline/Scan/ScanContext.cs | 149 ++++ .../Pipeline/Scan/ScanResult.cs | 28 + .../Pipeline/Scan/Scanner.cs | 392 +++++++++++ .../Pipeline/Scan/TokenCandidateInfo.cs | 54 ++ src/Rules.Framework.Rql/ReverseRqlBuilder.cs | 234 +++++++ src/Rules.Framework.Rql/RqlEngine.cs | 129 ++++ src/Rules.Framework.Rql/RqlEngineArgs.cs | 19 + src/Rules.Framework.Rql/RqlEngineBuilder.cs | 60 ++ src/Rules.Framework.Rql/RqlError.cs | 26 + src/Rules.Framework.Rql/RqlException.cs | 50 ++ src/Rules.Framework.Rql/RqlOptions.cs | 18 + src/Rules.Framework.Rql/RqlSourcePosition.cs | 24 + .../RuleEngineExtensions.cs | 29 + .../Rules.Framework.Rql.csproj | 15 + src/Rules.Framework.Rql/RulesSetResult.cs | 22 + src/Rules.Framework.Rql/RulesSetResultLine.cs | 19 + .../Runtime/IPropertySet.cs | 9 + src/Rules.Framework.Rql/Runtime/IRuntime.cs | 18 + .../Runtime/IRuntimeValue.cs | 14 + .../Runtime/MatchRulesArgs.cs | 17 + .../Runtime/RqlOperators.cs | 23 + src/Rules.Framework.Rql/Runtime/RqlRuntime.cs | 167 +++++ .../RuleManipulation/MatchCardinality.cs | 9 + .../Runtime/RuntimeException.cs | 23 + .../Runtime/SearchRulesArgs.cs | 16 + .../Runtime/Types/RqlAny.cs | 45 ++ .../Runtime/Types/RqlArray.cs | 119 ++++ .../Runtime/Types/RqlBool.cs | 33 + .../Runtime/Types/RqlDate.cs | 33 + .../Runtime/Types/RqlDecimal.cs | 33 + .../Runtime/Types/RqlInteger.cs | 33 + .../Runtime/Types/RqlNothing.cs | 21 + .../Runtime/Types/RqlObject.cs | 84 +++ .../Runtime/Types/RqlReadOnlyObject.cs | 81 +++ .../Runtime/Types/RqlRule.cs | 146 ++++ .../Runtime/Types/RqlString.cs | 34 + .../Runtime/Types/RqlType.cs | 55 ++ .../Runtime/Types/RqlTypes.cs | 55 ++ .../Tokens/AllowAsIdentifierAttribute.cs | 12 + src/Rules.Framework.Rql/Tokens/Constants.cs | 29 + src/Rules.Framework.Rql/Tokens/Token.cs | 49 ++ src/Rules.Framework.Rql/Tokens/TokenType.cs | 168 +++++ src/Rules.Framework.Rql/ValueResult.cs | 17 + src/Rules.Framework/IRulesEngine.cs | 2 +- .../AssemblyMetadata.cs | 3 + .../Rules.Framework.BenchmarkTests.csproj | 2 +- .../Tests/Benchmark3/Benchmark3.cs | 1 + .../Scenarios/IScenarioData.cs | 2 +- .../Scenarios/Scenario6/Scenario6Data.cs | 2 +- .../Scenarios/Scenario7/Scenario7Data.cs | 1 + .../Scenarios/Scenario8/ConditionTypes.cs | 2 +- .../Scenarios/Scenario8/ContentTypes.cs | 2 +- .../Scenario8/Scenario8Data.Flush.cs | 11 +- .../Scenario8/Scenario8Data.FourOfAKind.cs | 29 +- .../Scenario8/Scenario8Data.HighCard.cs | 29 +- .../Scenarios/Scenario8/Scenario8Data.Pair.cs | 29 +- .../Scenario8/Scenario8Data.RoyalFlush.cs | 10 +- .../Scenario8/Scenario8Data.Straight.cs | 18 +- .../Scenario8/Scenario8Data.StraightFlush.cs | 67 +- .../Scenario8/Scenario8Data.ThreeOfAKind.cs | 31 +- .../Scenarios/Scenario8/Scenario8Data.cs | 2 +- ...core.cs => SingleCombinationPokerScore.cs} | 2 +- .../Scenarios/ScenarioLoader.cs | 4 +- ...TexasHoldEmPokerSingleCombinationsTests.cs | 2 +- .../AssemblyMetadata.cs | 8 + .../CheckFiles/BasicLanguageChecks.yaml | 160 +++++ .../CheckFiles/MatchExpressionChecks.yaml | 113 +++ .../CheckFiles/SearchExpressionChecks.yaml | 121 ++++ .../GrammarCheck/GrammarCheckLine.cs | 10 + .../GrammarCheck/GrammarCheckTests.cs | 132 ++++ .../GrammarCheck/GrammarChecks.cs | 13 + ...ules.Framework.Rql.IntegrationTests.csproj | 46 ++ .../Scenarios/RqlMatchAllTestCase.cs | 9 + .../Scenarios/RqlMatchOneTestCase.cs | 9 + .../Scenarios/RqlScenarioTestCases.cs | 13 + .../Scenarios/RqlSearchTestCase.cs | 9 + .../RulesEngineWithScenario8RulesFixture.cs | 38 + .../Scenario8TestCasesLoaderFixture.cs | 39 ++ .../Scenarios/Scenario8/TestCases.yaml | 47 ++ ...TexasHoldEmPokerSingleCombinationsTests.cs | 139 ++++ .../AssemblyMetadata.cs | 7 + .../InterpreterTests.BinaryExpression.cs | 67 ++ .../InterpreterTests.CardinalitySegment.cs | 36 + .../InterpreterTests.ExpressionStatement.cs | 43 ++ .../InterpreterTests.IdentifierExpression.cs | 36 + .../InterpreterTests.InputConditionSegment.cs | 69 ++ ...InterpreterTests.InputConditionsSegment.cs | 40 ++ .../InterpreterTests.KeywordExpression.cs | 36 + .../InterpreterTests.LiteralExpression.cs | 74 ++ .../InterpreterTests.MatchExpression.cs | 146 ++++ .../InterpreterTests.NewArrayExpression.cs | 81 +++ .../InterpreterTests.NewObjectExpression.cs | 54 ++ .../Interpret/InterpreterTests.None.cs | 77 ++ .../InterpreterTests.OperatorSegment.cs | 89 +++ .../InterpreterTests.PlaceholderExpression.cs | 40 ++ .../InterpreterTests.SearchExpression.cs | 141 ++++ .../InterpreterTests.UnaryExpression.cs | 63 ++ .../Pipeline/Interpret/InterpreterTests.cs | 134 ++++ .../Pipeline/Parse/ParseContextTests.cs | 304 ++++++++ .../Pipeline/Parse/ParseStrategyPoolTests.cs | 118 ++++ .../Pipeline/Parse/StubParseStrategy.cs | 26 + .../Pipeline/Scan/ScanContextTests.cs | 205 ++++++ .../Pipeline/Scan/TokenCandidateInfoTests.cs | 71 ++ .../ReverseRqlBuilderTests.cs | 658 ++++++++++++++++++ .../RqlEngineBuilderTests.cs | 54 ++ .../RqlEngineTests.cs | 506 ++++++++++++++ .../Rules.Framework.Rql.Tests.csproj | 31 + .../RulesEngineExtensionsTests.cs | 49 ++ .../Runtime/RqlRuntimeTests.cs | 351 ++++++++++ .../TestStubs/ConditionType.cs | 15 + .../TestStubs/ContentType.cs | 9 + .../TestStubs/StubResult.cs | 14 + .../Rules.Framework.RqlReplTester/Program.cs | 195 ++++++ .../Rules.Framework.RqlReplTester.csproj | 26 + .../test-script.rql | 49 ++ .../Rules.Framework.Tests.csproj | 11 +- 197 files changed, 10349 insertions(+), 136 deletions(-) create mode 100644 src/Rules.Framework.Rql/AssemblyMetadata.cs create mode 100644 src/Rules.Framework.Rql/Ast/Expressions/AssignmentExpression.cs create mode 100644 src/Rules.Framework.Rql/Ast/Expressions/BinaryExpression.cs create mode 100644 src/Rules.Framework.Rql/Ast/Expressions/Expression.cs create mode 100644 src/Rules.Framework.Rql/Ast/Expressions/IExpressionVisitor.cs create mode 100644 src/Rules.Framework.Rql/Ast/Expressions/IdentifierExpression.cs create mode 100644 src/Rules.Framework.Rql/Ast/Expressions/KeywordExpression.cs create mode 100644 src/Rules.Framework.Rql/Ast/Expressions/LiteralExpression.cs create mode 100644 src/Rules.Framework.Rql/Ast/Expressions/LiteralType.cs create mode 100644 src/Rules.Framework.Rql/Ast/Expressions/MatchExpression.cs create mode 100644 src/Rules.Framework.Rql/Ast/Expressions/NewArrayExpression.cs create mode 100644 src/Rules.Framework.Rql/Ast/Expressions/NewObjectExpression.cs create mode 100644 src/Rules.Framework.Rql/Ast/Expressions/NoneExpression.cs create mode 100644 src/Rules.Framework.Rql/Ast/Expressions/PlaceholderExpression.cs create mode 100644 src/Rules.Framework.Rql/Ast/Expressions/SearchExpression.cs create mode 100644 src/Rules.Framework.Rql/Ast/Expressions/UnaryExpression.cs create mode 100644 src/Rules.Framework.Rql/Ast/IAstElement.cs create mode 100644 src/Rules.Framework.Rql/Ast/Segments/CardinalitySegment.cs create mode 100644 src/Rules.Framework.Rql/Ast/Segments/ISegmentVisitor.cs create mode 100644 src/Rules.Framework.Rql/Ast/Segments/InputConditionSegment.cs create mode 100644 src/Rules.Framework.Rql/Ast/Segments/InputConditionsSegment.cs create mode 100644 src/Rules.Framework.Rql/Ast/Segments/NoneSegment.cs create mode 100644 src/Rules.Framework.Rql/Ast/Segments/OperatorSegment.cs create mode 100644 src/Rules.Framework.Rql/Ast/Segments/Segment.cs create mode 100644 src/Rules.Framework.Rql/Ast/Statements/ExpressionStatement.cs create mode 100644 src/Rules.Framework.Rql/Ast/Statements/IStatementVisitor.cs create mode 100644 src/Rules.Framework.Rql/Ast/Statements/NoneStatement.cs create mode 100644 src/Rules.Framework.Rql/Ast/Statements/Statement.cs create mode 100644 src/Rules.Framework.Rql/IResult.cs create mode 100644 src/Rules.Framework.Rql/IReverseRqlBuilder.cs create mode 100644 src/Rules.Framework.Rql/IRqlEngine.cs create mode 100644 src/Rules.Framework.Rql/Messages/IMessageContainer.cs create mode 100644 src/Rules.Framework.Rql/Messages/Message.cs create mode 100644 src/Rules.Framework.Rql/Messages/MessageContainer.cs create mode 100644 src/Rules.Framework.Rql/Messages/MessageSeverity.cs create mode 100644 src/Rules.Framework.Rql/NothingResult.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Interpret/ErrorStatementResult.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Interpret/ExpressionStatementResult.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Interpret/IInterpreter.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Interpret/IResult.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Interpret/InterpretResult.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Interpret/Interpreter.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Interpret/InterpreterException.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Interpret/NothingStatementResult.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/IExpressionParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/IParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/IParseStrategyProvider.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/IParser.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/ISegmentParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/IStatementParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/PanicModeInfo.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/ParseContext.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/ParseResult.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/ParseStrategyPool.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Parser.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ArrayParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/AssignmentParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/BaseExpressionParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/CardinalityParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ContentTypeParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/DeclarationParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ExpressionParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ExpressionStatementParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/FactorParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/IdentifierParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/InputConditionParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/InputConditionsParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/KeywordParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/LiteralParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/MatchRulesParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/NothingParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ObjectParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/OperatorParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ParseStrategyBase.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/RulesManipulationParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/SearchRulesParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/StatementParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/TermParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Parse/Strategies/UnaryParseStrategy.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Scan/IScanner.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Scan/ScanContext.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Scan/ScanResult.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs create mode 100644 src/Rules.Framework.Rql/Pipeline/Scan/TokenCandidateInfo.cs create mode 100644 src/Rules.Framework.Rql/ReverseRqlBuilder.cs create mode 100644 src/Rules.Framework.Rql/RqlEngine.cs create mode 100644 src/Rules.Framework.Rql/RqlEngineArgs.cs create mode 100644 src/Rules.Framework.Rql/RqlEngineBuilder.cs create mode 100644 src/Rules.Framework.Rql/RqlError.cs create mode 100644 src/Rules.Framework.Rql/RqlException.cs create mode 100644 src/Rules.Framework.Rql/RqlOptions.cs create mode 100644 src/Rules.Framework.Rql/RqlSourcePosition.cs create mode 100644 src/Rules.Framework.Rql/RuleEngineExtensions.cs create mode 100644 src/Rules.Framework.Rql/Rules.Framework.Rql.csproj create mode 100644 src/Rules.Framework.Rql/RulesSetResult.cs create mode 100644 src/Rules.Framework.Rql/RulesSetResultLine.cs create mode 100644 src/Rules.Framework.Rql/Runtime/IPropertySet.cs create mode 100644 src/Rules.Framework.Rql/Runtime/IRuntime.cs create mode 100644 src/Rules.Framework.Rql/Runtime/IRuntimeValue.cs create mode 100644 src/Rules.Framework.Rql/Runtime/MatchRulesArgs.cs create mode 100644 src/Rules.Framework.Rql/Runtime/RqlOperators.cs create mode 100644 src/Rules.Framework.Rql/Runtime/RqlRuntime.cs create mode 100644 src/Rules.Framework.Rql/Runtime/RuleManipulation/MatchCardinality.cs create mode 100644 src/Rules.Framework.Rql/Runtime/RuntimeException.cs create mode 100644 src/Rules.Framework.Rql/Runtime/SearchRulesArgs.cs create mode 100644 src/Rules.Framework.Rql/Runtime/Types/RqlAny.cs create mode 100644 src/Rules.Framework.Rql/Runtime/Types/RqlArray.cs create mode 100644 src/Rules.Framework.Rql/Runtime/Types/RqlBool.cs create mode 100644 src/Rules.Framework.Rql/Runtime/Types/RqlDate.cs create mode 100644 src/Rules.Framework.Rql/Runtime/Types/RqlDecimal.cs create mode 100644 src/Rules.Framework.Rql/Runtime/Types/RqlInteger.cs create mode 100644 src/Rules.Framework.Rql/Runtime/Types/RqlNothing.cs create mode 100644 src/Rules.Framework.Rql/Runtime/Types/RqlObject.cs create mode 100644 src/Rules.Framework.Rql/Runtime/Types/RqlReadOnlyObject.cs create mode 100644 src/Rules.Framework.Rql/Runtime/Types/RqlRule.cs create mode 100644 src/Rules.Framework.Rql/Runtime/Types/RqlString.cs create mode 100644 src/Rules.Framework.Rql/Runtime/Types/RqlType.cs create mode 100644 src/Rules.Framework.Rql/Runtime/Types/RqlTypes.cs create mode 100644 src/Rules.Framework.Rql/Tokens/AllowAsIdentifierAttribute.cs create mode 100644 src/Rules.Framework.Rql/Tokens/Constants.cs create mode 100644 src/Rules.Framework.Rql/Tokens/Token.cs create mode 100644 src/Rules.Framework.Rql/Tokens/TokenType.cs create mode 100644 src/Rules.Framework.Rql/ValueResult.cs create mode 100644 tests/Rules.Framework.BenchmarkTests/AssemblyMetadata.cs rename tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/{CardPokerScore.cs => SingleCombinationPokerScore.cs} (68%) create mode 100644 tests/Rules.Framework.Rql.IntegrationTests/AssemblyMetadata.cs create mode 100644 tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/CheckFiles/BasicLanguageChecks.yaml create mode 100644 tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/CheckFiles/MatchExpressionChecks.yaml create mode 100644 tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/CheckFiles/SearchExpressionChecks.yaml create mode 100644 tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/GrammarCheckLine.cs create mode 100644 tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/GrammarCheckTests.cs create mode 100644 tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/GrammarChecks.cs create mode 100644 tests/Rules.Framework.Rql.IntegrationTests/Rules.Framework.Rql.IntegrationTests.csproj create mode 100644 tests/Rules.Framework.Rql.IntegrationTests/Scenarios/RqlMatchAllTestCase.cs create mode 100644 tests/Rules.Framework.Rql.IntegrationTests/Scenarios/RqlMatchOneTestCase.cs create mode 100644 tests/Rules.Framework.Rql.IntegrationTests/Scenarios/RqlScenarioTestCases.cs create mode 100644 tests/Rules.Framework.Rql.IntegrationTests/Scenarios/RqlSearchTestCase.cs create mode 100644 tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/RulesEngineWithScenario8RulesFixture.cs create mode 100644 tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/Scenario8TestCasesLoaderFixture.cs create mode 100644 tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/TestCases.yaml create mode 100644 tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/TexasHoldEmPokerSingleCombinationsTests.cs create mode 100644 tests/Rules.Framework.Rql.Tests/AssemblyMetadata.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.BinaryExpression.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.CardinalitySegment.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.ExpressionStatement.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.IdentifierExpression.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.InputConditionSegment.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.InputConditionsSegment.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.KeywordExpression.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.LiteralExpression.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.MatchExpression.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.NewArrayExpression.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.NewObjectExpression.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.None.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.OperatorSegment.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.PlaceholderExpression.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.SearchExpression.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.UnaryExpression.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Parse/ParseContextTests.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Parse/ParseStrategyPoolTests.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Parse/StubParseStrategy.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Scan/ScanContextTests.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Pipeline/Scan/TokenCandidateInfoTests.cs create mode 100644 tests/Rules.Framework.Rql.Tests/ReverseRqlBuilderTests.cs create mode 100644 tests/Rules.Framework.Rql.Tests/RqlEngineBuilderTests.cs create mode 100644 tests/Rules.Framework.Rql.Tests/RqlEngineTests.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Rules.Framework.Rql.Tests.csproj create mode 100644 tests/Rules.Framework.Rql.Tests/RulesEngineExtensionsTests.cs create mode 100644 tests/Rules.Framework.Rql.Tests/Runtime/RqlRuntimeTests.cs create mode 100644 tests/Rules.Framework.Rql.Tests/TestStubs/ConditionType.cs create mode 100644 tests/Rules.Framework.Rql.Tests/TestStubs/ContentType.cs create mode 100644 tests/Rules.Framework.Rql.Tests/TestStubs/StubResult.cs create mode 100644 tests/Rules.Framework.RqlReplTester/Program.cs create mode 100644 tests/Rules.Framework.RqlReplTester/Rules.Framework.RqlReplTester.csproj create mode 100644 tests/Rules.Framework.RqlReplTester/test-script.rql diff --git a/rules-framework.sln b/rules-framework.sln index 2e0f7a02..2369b569 100644 --- a/rules-framework.sln +++ b/rules-framework.sln @@ -40,6 +40,14 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rules.Framework.WebUI.Tests EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rules.Framework.BenchmarkTests", "tests\Rules.Framework.BenchmarkTests\Rules.Framework.BenchmarkTests.csproj", "{16C9F383-3B58-4911-9D26-7FDB907DD0D2}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rules.Framework.RqlReplTester", "tests\Rules.Framework.RqlReplTester\Rules.Framework.RqlReplTester.csproj", "{F21A8797-89E4-4EB3-92AF-4A051C3E579A}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rules.Framework.Rql", "src\Rules.Framework.Rql\Rules.Framework.Rql.csproj", "{76298D4D-537C-4522-91AB-0084535B1FF0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Rules.Framework.Rql.Tests", "tests\Rules.Framework.Rql.Tests\Rules.Framework.Rql.Tests.csproj", "{776E54A7-9099-4EBD-9C62-A371DFED58E5}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Rules.Framework.Rql.IntegrationTests", "tests\Rules.Framework.Rql.IntegrationTests\Rules.Framework.Rql.IntegrationTests.csproj", "{C24A2234-AD6A-4377-9FAA-9CC58386107C}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -98,6 +106,22 @@ Global {16C9F383-3B58-4911-9D26-7FDB907DD0D2}.Debug|Any CPU.Build.0 = Debug|Any CPU {16C9F383-3B58-4911-9D26-7FDB907DD0D2}.Release|Any CPU.ActiveCfg = Release|Any CPU {16C9F383-3B58-4911-9D26-7FDB907DD0D2}.Release|Any CPU.Build.0 = Release|Any CPU + {F21A8797-89E4-4EB3-92AF-4A051C3E579A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F21A8797-89E4-4EB3-92AF-4A051C3E579A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F21A8797-89E4-4EB3-92AF-4A051C3E579A}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F21A8797-89E4-4EB3-92AF-4A051C3E579A}.Release|Any CPU.Build.0 = Release|Any CPU + {76298D4D-537C-4522-91AB-0084535B1FF0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {76298D4D-537C-4522-91AB-0084535B1FF0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {76298D4D-537C-4522-91AB-0084535B1FF0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {76298D4D-537C-4522-91AB-0084535B1FF0}.Release|Any CPU.Build.0 = Release|Any CPU + {776E54A7-9099-4EBD-9C62-A371DFED58E5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {776E54A7-9099-4EBD-9C62-A371DFED58E5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {776E54A7-9099-4EBD-9C62-A371DFED58E5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {776E54A7-9099-4EBD-9C62-A371DFED58E5}.Release|Any CPU.Build.0 = Release|Any CPU + {C24A2234-AD6A-4377-9FAA-9CC58386107C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C24A2234-AD6A-4377-9FAA-9CC58386107C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C24A2234-AD6A-4377-9FAA-9CC58386107C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C24A2234-AD6A-4377-9FAA-9CC58386107C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -116,6 +140,10 @@ Global {7CE82611-FEC1-49E9-91FB-4C3ADF5ED56F} = {AEE746EC-CEAA-4892-8C29-0CAAB97A23A8} {29DC6661-4F0C-46F7-AC91-968700D13C11} = {74E24C97-8EE4-4B69-AECD-4765FD2C751F} {16C9F383-3B58-4911-9D26-7FDB907DD0D2} = {74E24C97-8EE4-4B69-AECD-4765FD2C751F} + {F21A8797-89E4-4EB3-92AF-4A051C3E579A} = {74E24C97-8EE4-4B69-AECD-4765FD2C751F} + {76298D4D-537C-4522-91AB-0084535B1FF0} = {AEE746EC-CEAA-4892-8C29-0CAAB97A23A8} + {776E54A7-9099-4EBD-9C62-A371DFED58E5} = {74E24C97-8EE4-4B69-AECD-4765FD2C751F} + {C24A2234-AD6A-4377-9FAA-9CC58386107C} = {74E24C97-8EE4-4B69-AECD-4765FD2C751F} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FA9D4C31-972B-49C2-9F63-C56ED766DAB0} diff --git a/src/Rules.Framework.Rql/AssemblyMetadata.cs b/src/Rules.Framework.Rql/AssemblyMetadata.cs new file mode 100644 index 00000000..d29c91c6 --- /dev/null +++ b/src/Rules.Framework.Rql/AssemblyMetadata.cs @@ -0,0 +1,5 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Rules.Framework.Rql.Tests")] +[assembly: InternalsVisibleTo("Rules.Framework.Rql.IntegrationTests")] +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Expressions/AssignmentExpression.cs b/src/Rules.Framework.Rql/Ast/Expressions/AssignmentExpression.cs new file mode 100644 index 00000000..349e6a96 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Expressions/AssignmentExpression.cs @@ -0,0 +1,26 @@ +namespace Rules.Framework.Rql.Ast.Expressions +{ + using System; + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql.Tokens; + + [ExcludeFromCodeCoverage] + internal class AssignmentExpression : Expression + { + public AssignmentExpression(Expression left, Token assign, Expression right) + : base(left.BeginPosition, right.EndPosition) + { + this.Left = left; + this.Assign = assign; + this.Right = right; + } + + public Token Assign { get; } + + public Expression Left { get; } + + public Expression Right { get; } + + public override T Accept(IExpressionVisitor visitor) => throw new NotSupportedException("To be supported in a future release."); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Expressions/BinaryExpression.cs b/src/Rules.Framework.Rql/Ast/Expressions/BinaryExpression.cs new file mode 100644 index 00000000..7b654085 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Expressions/BinaryExpression.cs @@ -0,0 +1,25 @@ +namespace Rules.Framework.Rql.Ast.Expressions +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql.Ast.Segments; + + [ExcludeFromCodeCoverage] + internal class BinaryExpression : Expression + { + public BinaryExpression(Expression leftExpression, Segment operatorSegment, Expression rightExpression) + : base(leftExpression.BeginPosition, rightExpression.EndPosition) + { + this.LeftExpression = leftExpression; + this.OperatorSegment = operatorSegment; + this.RightExpression = rightExpression; + } + + public Expression LeftExpression { get; } + + public Segment OperatorSegment { get; } + + public Expression RightExpression { get; } + + public override T Accept(IExpressionVisitor visitor) => visitor.VisitBinaryExpression(this); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Expressions/Expression.cs b/src/Rules.Framework.Rql/Ast/Expressions/Expression.cs new file mode 100644 index 00000000..a85bb42c --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Expressions/Expression.cs @@ -0,0 +1,24 @@ +namespace Rules.Framework.Rql.Ast.Expressions +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Tokens; + + [ExcludeFromCodeCoverage] + internal abstract class Expression : IAstElement + { + protected Expression(RqlSourcePosition beginPosition, RqlSourcePosition endPosition) + { + this.BeginPosition = beginPosition; + this.EndPosition = endPosition; + } + + public static Expression None { get; } = new NoneExpression(); + + public RqlSourcePosition BeginPosition { get; } + + public RqlSourcePosition EndPosition { get; } + + public abstract T Accept(IExpressionVisitor visitor); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Expressions/IExpressionVisitor.cs b/src/Rules.Framework.Rql/Ast/Expressions/IExpressionVisitor.cs new file mode 100644 index 00000000..78e7ae27 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Expressions/IExpressionVisitor.cs @@ -0,0 +1,29 @@ +namespace Rules.Framework.Rql.Ast.Expressions +{ + internal interface IExpressionVisitor + { + T VisitAssignmentExpression(AssignmentExpression assignmentExpression); + + T VisitBinaryExpression(BinaryExpression binaryExpression); + + T VisitIdentifierExpression(IdentifierExpression identifierExpression); + + T VisitKeywordExpression(KeywordExpression keywordExpression); + + T VisitLiteralExpression(LiteralExpression literalExpression); + + T VisitMatchExpression(MatchExpression matchExpression); + + T VisitNewArrayExpression(NewArrayExpression newArrayExpression); + + T VisitNewObjectExpression(NewObjectExpression newObjectExpression); + + T VisitNoneExpression(NoneExpression noneExpression); + + T VisitPlaceholderExpression(PlaceholderExpression placeholderExpression); + + T VisitSearchExpression(SearchExpression searchExpression); + + T VisitUnaryExpression(UnaryExpression expression); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Expressions/IdentifierExpression.cs b/src/Rules.Framework.Rql/Ast/Expressions/IdentifierExpression.cs new file mode 100644 index 00000000..b72750b7 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Expressions/IdentifierExpression.cs @@ -0,0 +1,19 @@ +namespace Rules.Framework.Rql.Ast.Expressions +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql.Tokens; + + [ExcludeFromCodeCoverage] + internal class IdentifierExpression : Expression + { + public IdentifierExpression(Token identifier) + : base(identifier.BeginPosition, identifier.EndPosition) + { + this.Identifier = identifier; + } + + public Token Identifier { get; } + + public override T Accept(IExpressionVisitor visitor) => visitor.VisitIdentifierExpression(this); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Expressions/KeywordExpression.cs b/src/Rules.Framework.Rql/Ast/Expressions/KeywordExpression.cs new file mode 100644 index 00000000..190ec3a4 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Expressions/KeywordExpression.cs @@ -0,0 +1,22 @@ +namespace Rules.Framework.Rql.Ast.Expressions +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql.Tokens; + + [ExcludeFromCodeCoverage] + internal class KeywordExpression : Expression + { + private KeywordExpression(Token keyword) + : base(keyword.BeginPosition, keyword.EndPosition) + { + this.Keyword = keyword; + } + + public Token Keyword { get; } + + public static KeywordExpression Create(Token keyword) + => new(keyword); + + public override T Accept(IExpressionVisitor visitor) => visitor.VisitKeywordExpression(this); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Expressions/LiteralExpression.cs b/src/Rules.Framework.Rql/Ast/Expressions/LiteralExpression.cs new file mode 100644 index 00000000..e16ceaac --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Expressions/LiteralExpression.cs @@ -0,0 +1,28 @@ +namespace Rules.Framework.Rql.Ast.Expressions +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql.Tokens; + + [ExcludeFromCodeCoverage] + internal class LiteralExpression : Expression + { + private LiteralExpression(LiteralType type, Token token, object value) + : base(token.BeginPosition, token.EndPosition) + { + this.Type = type; + this.Token = token; + this.Value = value; + } + + public Token Token { get; } + + public LiteralType Type { get; } + + public object Value { get; } + + public static LiteralExpression Create(LiteralType type, Token token, object value) + => new(type, token, value); + + public override T Accept(IExpressionVisitor visitor) => visitor.VisitLiteralExpression(this); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Expressions/LiteralType.cs b/src/Rules.Framework.Rql/Ast/Expressions/LiteralType.cs new file mode 100644 index 00000000..0fc25bc4 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Expressions/LiteralType.cs @@ -0,0 +1,12 @@ +namespace Rules.Framework.Rql.Ast.Expressions +{ + internal enum LiteralType + { + Undefined = 0, + String = 1, + Integer = 2, + Decimal = 3, + Bool = 4, + DateTime = 5, + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Expressions/MatchExpression.cs b/src/Rules.Framework.Rql/Ast/Expressions/MatchExpression.cs new file mode 100644 index 00000000..76171790 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Expressions/MatchExpression.cs @@ -0,0 +1,38 @@ +namespace Rules.Framework.Rql.Ast.Expressions +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql.Ast.Segments; + + [ExcludeFromCodeCoverage] + internal class MatchExpression : Expression + { + private MatchExpression( + Segment cardinality, + Expression contentType, + Expression matchDate, + Segment inputConditions) + : base(cardinality.BeginPosition, inputConditions?.EndPosition ?? matchDate.EndPosition) + { + this.Cardinality = cardinality; + this.ContentType = contentType; + this.MatchDate = matchDate; + this.InputConditions = inputConditions; + } + + public Segment Cardinality { get; } + + public Expression ContentType { get; } + + public Segment InputConditions { get; } + + public Expression MatchDate { get; } + + public static MatchExpression Create(Segment cardinality, + Expression contentType, + Expression matchDate, + Segment inputConditions) + => new(cardinality, contentType, matchDate, inputConditions); + + public override T Accept(IExpressionVisitor visitor) => visitor.VisitMatchExpression(this); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Expressions/NewArrayExpression.cs b/src/Rules.Framework.Rql/Ast/Expressions/NewArrayExpression.cs new file mode 100644 index 00000000..c18d22e6 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Expressions/NewArrayExpression.cs @@ -0,0 +1,34 @@ +namespace Rules.Framework.Rql.Ast.Expressions +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql.Tokens; + + [ExcludeFromCodeCoverage] + internal class NewArrayExpression : Expression + { + private NewArrayExpression(Token array, Token initializerBeginToken, Expression size, Expression[] values, Token initializerEndToken) + : base(array.EndPosition, initializerEndToken.EndPosition) + { + this.Array = array; + this.InitializerBeginToken = initializerBeginToken; + this.Size = size; + this.Values = values; + this.InitializerEndToken = initializerEndToken; + } + + public Token Array { get; } + + public Token InitializerBeginToken { get; } + + public Token InitializerEndToken { get; } + + public Expression Size { get; } + + public Expression[] Values { get; } + + public static NewArrayExpression Create(Token array, Token initializerBeginToken, Expression size, Expression[] values, Token initializerEndToken) + => new(array, initializerBeginToken, size, values, initializerEndToken); + + public override T Accept(IExpressionVisitor visitor) => visitor.VisitNewArrayExpression(this); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Expressions/NewObjectExpression.cs b/src/Rules.Framework.Rql/Ast/Expressions/NewObjectExpression.cs new file mode 100644 index 00000000..347b12fd --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Expressions/NewObjectExpression.cs @@ -0,0 +1,23 @@ +namespace Rules.Framework.Rql.Ast.Expressions +{ + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using Rules.Framework.Rql.Tokens; + + [ExcludeFromCodeCoverage] + internal class NewObjectExpression : Expression + { + public NewObjectExpression(Token @object, Expression[] propertyAssignements) + : base(@object.BeginPosition, propertyAssignements.LastOrDefault()?.EndPosition ?? @object.EndPosition) + { + this.Object = @object; + this.PropertyAssignments = propertyAssignements; + } + + public Token Object { get; } + + public Expression[] PropertyAssignments { get; } + + public override T Accept(IExpressionVisitor visitor) => visitor.VisitNewObjectExpression(this); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Expressions/NoneExpression.cs b/src/Rules.Framework.Rql/Ast/Expressions/NoneExpression.cs new file mode 100644 index 00000000..c1d79de1 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Expressions/NoneExpression.cs @@ -0,0 +1,15 @@ +namespace Rules.Framework.Rql.Ast.Expressions +{ + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + internal class NoneExpression : Expression + { + public NoneExpression() + : base(RqlSourcePosition.Empty, RqlSourcePosition.Empty) + { + } + + public override T Accept(IExpressionVisitor visitor) => visitor.VisitNoneExpression(this); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Expressions/PlaceholderExpression.cs b/src/Rules.Framework.Rql/Ast/Expressions/PlaceholderExpression.cs new file mode 100644 index 00000000..89a337ce --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Expressions/PlaceholderExpression.cs @@ -0,0 +1,19 @@ +namespace Rules.Framework.Rql.Ast.Expressions +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql.Tokens; + + [ExcludeFromCodeCoverage] + internal class PlaceholderExpression : Expression + { + public PlaceholderExpression(Token token) + : base(token.BeginPosition, token.EndPosition) + { + this.Token = token; + } + + public Token Token { get; } + + public override T Accept(IExpressionVisitor visitor) => visitor.VisitPlaceholderExpression(this); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Expressions/SearchExpression.cs b/src/Rules.Framework.Rql/Ast/Expressions/SearchExpression.cs new file mode 100644 index 00000000..84926017 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Expressions/SearchExpression.cs @@ -0,0 +1,31 @@ +namespace Rules.Framework.Rql.Ast.Expressions +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql.Ast.Segments; + + [ExcludeFromCodeCoverage] + internal class SearchExpression : Expression + { + public SearchExpression(Expression contentType, + Expression dateBegin, + Expression dateEnd, + Segment inputConditions) + : base(contentType.BeginPosition, inputConditions?.EndPosition ?? dateEnd.EndPosition) + { + this.ContentType = contentType; + this.DateBegin = dateBegin; + this.DateEnd = dateEnd; + this.InputConditions = inputConditions; + } + + public Expression ContentType { get; } + + public Expression DateBegin { get; } + + public Expression DateEnd { get; } + + public Segment InputConditions { get; } + + public override T Accept(IExpressionVisitor visitor) => visitor.VisitSearchExpression(this); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Expressions/UnaryExpression.cs b/src/Rules.Framework.Rql/Ast/Expressions/UnaryExpression.cs new file mode 100644 index 00000000..f9cbfe1a --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Expressions/UnaryExpression.cs @@ -0,0 +1,22 @@ +namespace Rules.Framework.Rql.Ast.Expressions +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql.Tokens; + + [ExcludeFromCodeCoverage] + internal class UnaryExpression : Expression + { + public UnaryExpression(Token @operator, Expression right) + : base(@operator.BeginPosition, right.EndPosition) + { + this.Operator = @operator; + this.Right = right; + } + + public Token Operator { get; } + + public Expression Right { get; } + + public override T Accept(IExpressionVisitor visitor) => visitor.VisitUnaryExpression(this); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/IAstElement.cs b/src/Rules.Framework.Rql/Ast/IAstElement.cs new file mode 100644 index 00000000..c7586025 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/IAstElement.cs @@ -0,0 +1,13 @@ +namespace Rules.Framework.Rql.Ast +{ + using System.Collections.Generic; + using System.Linq; + using Rules.Framework.Rql.Tokens; + + internal interface IAstElement + { + RqlSourcePosition BeginPosition { get; } + + RqlSourcePosition EndPosition { get; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Segments/CardinalitySegment.cs b/src/Rules.Framework.Rql/Ast/Segments/CardinalitySegment.cs new file mode 100644 index 00000000..9421754d --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Segments/CardinalitySegment.cs @@ -0,0 +1,25 @@ +namespace Rules.Framework.Rql.Ast.Segments +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql.Ast.Expressions; + + [ExcludeFromCodeCoverage] + internal class CardinalitySegment : Segment + { + public CardinalitySegment(Expression cardinalityKeyword, Expression ruleKeyword) + : base(cardinalityKeyword.BeginPosition, ruleKeyword.EndPosition) + { + this.CardinalityKeyword = cardinalityKeyword; + this.RuleKeyword = ruleKeyword; + } + + public Expression CardinalityKeyword { get; } + + public Expression RuleKeyword { get; } + + public static CardinalitySegment Create(Expression cardinalityKeyword, Expression ruleKeyword) + => new(cardinalityKeyword, ruleKeyword); + + public override T Accept(ISegmentVisitor visitor) => visitor.VisitCardinalitySegment(this); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Segments/ISegmentVisitor.cs b/src/Rules.Framework.Rql/Ast/Segments/ISegmentVisitor.cs new file mode 100644 index 00000000..637a4716 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Segments/ISegmentVisitor.cs @@ -0,0 +1,15 @@ +namespace Rules.Framework.Rql.Ast.Segments +{ + internal interface ISegmentVisitor + { + T VisitCardinalitySegment(CardinalitySegment cardinalitySegment); + + T VisitInputConditionSegment(InputConditionSegment inputConditionSegment); + + T VisitInputConditionsSegment(InputConditionsSegment inputConditionsSegment); + + T VisitNoneSegment(NoneSegment noneSegment); + + T VisitOperatorSegment(OperatorSegment operatorSegment); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Segments/InputConditionSegment.cs b/src/Rules.Framework.Rql/Ast/Segments/InputConditionSegment.cs new file mode 100644 index 00000000..f62851ba --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Segments/InputConditionSegment.cs @@ -0,0 +1,27 @@ +using Rules.Framework.Rql.Ast.Expressions; + +namespace Rules.Framework.Rql.Ast.Segments +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql.Tokens; + + [ExcludeFromCodeCoverage] + internal class InputConditionSegment : Segment + { + public InputConditionSegment(Expression left, Token @operator, Expression right) + : base(left.BeginPosition, right.EndPosition) + { + this.Left = left; + this.Operator = @operator; + this.Right = right; + } + + public Expression Left { get; } + + public Token Operator { get; } + + public Expression Right { get; } + + public override T Accept(ISegmentVisitor visitor) => visitor.VisitInputConditionSegment(this); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Segments/InputConditionsSegment.cs b/src/Rules.Framework.Rql/Ast/Segments/InputConditionsSegment.cs new file mode 100644 index 00000000..fff633b2 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Segments/InputConditionsSegment.cs @@ -0,0 +1,19 @@ +namespace Rules.Framework.Rql.Ast.Segments +{ + using System.Diagnostics.CodeAnalysis; + using System.Linq; + + [ExcludeFromCodeCoverage] + internal class InputConditionsSegment : Segment + { + public InputConditionsSegment(Segment[] inputConditions) + : base(inputConditions.FirstOrDefault()?.BeginPosition ?? RqlSourcePosition.Empty, inputConditions.LastOrDefault()?.EndPosition ?? RqlSourcePosition.Empty) + { + this.InputConditions = inputConditions; + } + + public Segment[] InputConditions { get; } + + public override T Accept(ISegmentVisitor visitor) => visitor.VisitInputConditionsSegment(this); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Segments/NoneSegment.cs b/src/Rules.Framework.Rql/Ast/Segments/NoneSegment.cs new file mode 100644 index 00000000..259847d0 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Segments/NoneSegment.cs @@ -0,0 +1,15 @@ +namespace Rules.Framework.Rql.Ast.Segments +{ + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + internal class NoneSegment : Segment + { + public NoneSegment() + : base(RqlSourcePosition.Empty, RqlSourcePosition.Empty) + { + } + + public override T Accept(ISegmentVisitor visitor) => visitor.VisitNoneSegment(this); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Segments/OperatorSegment.cs b/src/Rules.Framework.Rql/Ast/Segments/OperatorSegment.cs new file mode 100644 index 00000000..51661fb1 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Segments/OperatorSegment.cs @@ -0,0 +1,19 @@ +namespace Rules.Framework.Rql.Ast.Segments +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql.Tokens; + + [ExcludeFromCodeCoverage] + internal class OperatorSegment : Segment + { + public OperatorSegment(Token[] tokens) + : base(tokens[0].BeginPosition, tokens[tokens.Length - 1].EndPosition) + { + this.Tokens = tokens; + } + + public Token[] Tokens { get; } + + public override T Accept(ISegmentVisitor visitor) => visitor.VisitOperatorSegment(this); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Segments/Segment.cs b/src/Rules.Framework.Rql/Ast/Segments/Segment.cs new file mode 100644 index 00000000..e9c50237 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Segments/Segment.cs @@ -0,0 +1,23 @@ +namespace Rules.Framework.Rql.Ast.Segments +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql; + + [ExcludeFromCodeCoverage] + internal abstract class Segment : IAstElement + { + protected Segment(RqlSourcePosition beginPosition, RqlSourcePosition endPosition) + { + this.BeginPosition = beginPosition; + this.EndPosition = endPosition; + } + + public static Segment None { get; } = new NoneSegment(); + + public RqlSourcePosition BeginPosition { get; } + + public RqlSourcePosition EndPosition { get; } + + public abstract T Accept(ISegmentVisitor visitor); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Statements/ExpressionStatement.cs b/src/Rules.Framework.Rql/Ast/Statements/ExpressionStatement.cs new file mode 100644 index 00000000..468d6446 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Statements/ExpressionStatement.cs @@ -0,0 +1,22 @@ +namespace Rules.Framework.Rql.Ast.Statements +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql.Ast.Expressions; + + [ExcludeFromCodeCoverage] + internal class ExpressionStatement : Statement + { + private ExpressionStatement(Expression expression) + : base(expression.BeginPosition, expression.EndPosition) + { + this.Expression = expression; + } + + public Expression Expression { get; } + + public static ExpressionStatement Create(Expression expression) + => new(expression); + + public override T Accept(IStatementVisitor visitor) => visitor.VisitExpressionStatement(this); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Statements/IStatementVisitor.cs b/src/Rules.Framework.Rql/Ast/Statements/IStatementVisitor.cs new file mode 100644 index 00000000..61f3cbe1 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Statements/IStatementVisitor.cs @@ -0,0 +1,9 @@ +namespace Rules.Framework.Rql.Ast.Statements +{ + internal interface IStatementVisitor + { + T VisitExpressionStatement(ExpressionStatement expressionStatement); + + T VisitNoneStatement(NoneStatement noneStatement); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Statements/NoneStatement.cs b/src/Rules.Framework.Rql/Ast/Statements/NoneStatement.cs new file mode 100644 index 00000000..c3759229 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Statements/NoneStatement.cs @@ -0,0 +1,15 @@ +namespace Rules.Framework.Rql.Ast.Statements +{ + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + internal class NoneStatement : Statement + { + public NoneStatement() + : base(RqlSourcePosition.Empty, RqlSourcePosition.Empty) + { + } + + public override T Accept(IStatementVisitor visitor) => visitor.VisitNoneStatement(this); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Ast/Statements/Statement.cs b/src/Rules.Framework.Rql/Ast/Statements/Statement.cs new file mode 100644 index 00000000..c2a1c357 --- /dev/null +++ b/src/Rules.Framework.Rql/Ast/Statements/Statement.cs @@ -0,0 +1,23 @@ +namespace Rules.Framework.Rql.Ast.Statements +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql; + + [ExcludeFromCodeCoverage] + internal abstract class Statement : IAstElement + { + protected Statement(RqlSourcePosition beginPosition, RqlSourcePosition endPosition) + { + this.BeginPosition = beginPosition; + this.EndPosition = endPosition; + } + + public static Statement None { get; } = new NoneStatement(); + + public RqlSourcePosition BeginPosition { get; } + + public RqlSourcePosition EndPosition { get; } + + public abstract T Accept(IStatementVisitor visitor); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/IResult.cs b/src/Rules.Framework.Rql/IResult.cs new file mode 100644 index 00000000..1cf64866 --- /dev/null +++ b/src/Rules.Framework.Rql/IResult.cs @@ -0,0 +1,7 @@ +namespace Rules.Framework.Rql +{ + public interface IResult + { + string Rql { get; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/IReverseRqlBuilder.cs b/src/Rules.Framework.Rql/IReverseRqlBuilder.cs new file mode 100644 index 00000000..3dc41460 --- /dev/null +++ b/src/Rules.Framework.Rql/IReverseRqlBuilder.cs @@ -0,0 +1,9 @@ +namespace Rules.Framework.Rql +{ + using Rules.Framework.Rql.Ast; + + internal interface IReverseRqlBuilder + { + string BuildRql(IAstElement astElement); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/IRqlEngine.cs b/src/Rules.Framework.Rql/IRqlEngine.cs new file mode 100644 index 00000000..265854a2 --- /dev/null +++ b/src/Rules.Framework.Rql/IRqlEngine.cs @@ -0,0 +1,11 @@ +namespace Rules.Framework.Rql +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + + public interface IRqlEngine : IDisposable + { + Task> ExecuteAsync(string rql); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Messages/IMessageContainer.cs b/src/Rules.Framework.Rql/Messages/IMessageContainer.cs new file mode 100644 index 00000000..5ba2ea6a --- /dev/null +++ b/src/Rules.Framework.Rql/Messages/IMessageContainer.cs @@ -0,0 +1,11 @@ +namespace Rules.Framework.Rql.Messages +{ + using System; + + internal interface IMessageContainer : IDisposable + { + void Error(string message, RqlSourcePosition beginPosition, RqlSourcePosition endPosition); + + void Warning(string message, RqlSourcePosition beginPosition, RqlSourcePosition endPosition); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Messages/Message.cs b/src/Rules.Framework.Rql/Messages/Message.cs new file mode 100644 index 00000000..d9837bdb --- /dev/null +++ b/src/Rules.Framework.Rql/Messages/Message.cs @@ -0,0 +1,27 @@ +namespace Rules.Framework.Rql.Messages +{ + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + internal class Message + { + private Message(string text, RqlSourcePosition beginPosition, RqlSourcePosition endPosition, MessageSeverity severity) + { + this.Text = text; + this.BeginPosition = beginPosition; + this.EndPosition = endPosition; + this.Severity = severity; + } + + public RqlSourcePosition BeginPosition { get; } + + public RqlSourcePosition EndPosition { get; } + + public MessageSeverity Severity { get; } + + public string Text { get; } + + public static Message Create(string text, RqlSourcePosition beginPosition, RqlSourcePosition endPosition, MessageSeverity severity) + => new(text, beginPosition, endPosition, severity); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Messages/MessageContainer.cs b/src/Rules.Framework.Rql/Messages/MessageContainer.cs new file mode 100644 index 00000000..70e5455d --- /dev/null +++ b/src/Rules.Framework.Rql/Messages/MessageContainer.cs @@ -0,0 +1,68 @@ +namespace Rules.Framework.Rql.Messages +{ + using System; + using System.Collections.Generic; + + internal class MessageContainer : IMessageContainer + { + private bool disposedValue; + private List messages; + + public MessageContainer() + { + this.messages = new List(); + this.ErrorsCount = 0; + this.WarningsCount = 0; + } + + ~MessageContainer() + { + Dispose(disposing: false); + } + + public int ErrorsCount { get; private set; } + public IReadOnlyList Messages => this.messages.AsReadOnly(); + public int WarningsCount { get; private set; } + + public void Dispose() + { + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public void Error(string message, RqlSourcePosition beginPosition, RqlSourcePosition endPosition) + { + this.AddMessage(message, MessageSeverity.Error, beginPosition, endPosition); + this.ErrorsCount++; + } + + public void Warning(string message, RqlSourcePosition beginPosition, RqlSourcePosition endPosition) + { + this.AddMessage(message, MessageSeverity.Warning, beginPosition, endPosition); + this.WarningsCount++; + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + this.messages = null; + } + + disposedValue = true; + } + } + + private void AddMessage(string message, MessageSeverity severity, RqlSourcePosition beginPosition, RqlSourcePosition endPosition) + { + if (string.IsNullOrWhiteSpace(message)) + { + throw new ArgumentNullException(nameof(message)); + } + + this.messages.Add(Message.Create(message, beginPosition, endPosition, severity)); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Messages/MessageSeverity.cs b/src/Rules.Framework.Rql/Messages/MessageSeverity.cs new file mode 100644 index 00000000..1a28ffad --- /dev/null +++ b/src/Rules.Framework.Rql/Messages/MessageSeverity.cs @@ -0,0 +1,9 @@ +namespace Rules.Framework.Rql.Messages +{ + internal enum MessageSeverity + { + None = 0, + Error = 1, + Warning = 2, + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/NothingResult.cs b/src/Rules.Framework.Rql/NothingResult.cs new file mode 100644 index 00000000..5bf5d60b --- /dev/null +++ b/src/Rules.Framework.Rql/NothingResult.cs @@ -0,0 +1,15 @@ +namespace Rules.Framework.Rql +{ + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + public class NothingResult : IResult + { + public NothingResult(string rql) + { + this.Rql = rql; + } + + public string Rql { get; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Interpret/ErrorStatementResult.cs b/src/Rules.Framework.Rql/Pipeline/Interpret/ErrorStatementResult.cs new file mode 100644 index 00000000..083d17ea --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Interpret/ErrorStatementResult.cs @@ -0,0 +1,28 @@ +namespace Rules.Framework.Rql.Pipeline.Interpret +{ + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + internal class ErrorStatementResult : IResult + { + public ErrorStatementResult(string message, string rql, RqlSourcePosition beginPosition, RqlSourcePosition endPosition) + { + this.Message = message; + this.Rql = rql; + this.BeginPosition = beginPosition; + this.EndPosition = endPosition; + } + + public RqlSourcePosition BeginPosition { get; } + + public RqlSourcePosition EndPosition { get; } + + public bool HasOutput => false; + + public string Message { get; } + + public string Rql { get; } + + public bool Success => false; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Interpret/ExpressionStatementResult.cs b/src/Rules.Framework.Rql/Pipeline/Interpret/ExpressionStatementResult.cs new file mode 100644 index 00000000..907d7d2f --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Interpret/ExpressionStatementResult.cs @@ -0,0 +1,22 @@ +namespace Rules.Framework.Rql.Pipeline.Interpret +{ + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + internal class ExpressionStatementResult : IResult + { + public ExpressionStatementResult(string rql, object result) + { + this.Rql = rql; + this.Result = result; + } + + public bool HasOutput => true; + + public object Result { get; } + + public string Rql { get; } + + public bool Success => true; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Interpret/IInterpreter.cs b/src/Rules.Framework.Rql/Pipeline/Interpret/IInterpreter.cs new file mode 100644 index 00000000..ad3cb5fa --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Interpret/IInterpreter.cs @@ -0,0 +1,11 @@ +namespace Rules.Framework.Rql.Pipeline.Interpret +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using Rules.Framework.Rql.Ast.Statements; + + internal interface IInterpreter + { + Task InterpretAsync(IReadOnlyList statements); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Interpret/IResult.cs b/src/Rules.Framework.Rql/Pipeline/Interpret/IResult.cs new file mode 100644 index 00000000..b34df11c --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Interpret/IResult.cs @@ -0,0 +1,11 @@ +namespace Rules.Framework.Rql.Pipeline.Interpret +{ + internal interface IResult + { + bool HasOutput { get; } + + string Rql { get; } + + bool Success { get; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Interpret/InterpretResult.cs b/src/Rules.Framework.Rql/Pipeline/Interpret/InterpretResult.cs new file mode 100644 index 00000000..3b1f08b1 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Interpret/InterpretResult.cs @@ -0,0 +1,32 @@ +namespace Rules.Framework.Rql.Pipeline.Interpret +{ + using System; + using System.Collections.Generic; + + internal class InterpretResult + { + private readonly List results; + + public InterpretResult() + { + this.results = new List(); + this.Success = true; + } + + public IEnumerable Results => this.results.AsReadOnly(); + + public bool Success { get; private set; } + + public void AddStatementResult(IResult result) + { + if (result is null) + { + throw new ArgumentNullException(nameof(result)); + } + + this.results.Add(result); + + this.Success &= result.Success; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Interpret/Interpreter.cs b/src/Rules.Framework.Rql/Pipeline/Interpret/Interpreter.cs new file mode 100644 index 00000000..62ae84d6 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Interpret/Interpreter.cs @@ -0,0 +1,371 @@ +namespace Rules.Framework.Rql.Pipeline.Interpret +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Rules.Framework.Rql.Ast; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Ast.Segments; + using Rules.Framework.Rql.Ast.Statements; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Runtime.RuleManipulation; + using Rules.Framework.Rql.Runtime.Types; + using Rules.Framework.Rql.Tokens; + + internal class Interpreter : IInterpreter, IExpressionVisitor>, ISegmentVisitor>, IStatementVisitor> + { + private readonly IReverseRqlBuilder reverseRqlBuilder; + private bool disposedValue; + private IRuntime runtime; + + public Interpreter( + IRuntime runtime, + IReverseRqlBuilder reverseRqlBuilder) + { + this.runtime = runtime; + this.reverseRqlBuilder = reverseRqlBuilder; + } + + public async Task InterpretAsync(IReadOnlyList statements) + { + var interpretResult = new InterpretResult(); + foreach (var statement in statements) + { + try + { + var statementResult = await statement.Accept(this).ConfigureAwait(false); + interpretResult.AddStatementResult(statementResult); + } + catch (InterpreterException ie) + { + var errorStatementResult = new ErrorStatementResult(ie.Message, ie.Rql, ie.BeginPosition, ie.EndPosition); + interpretResult.AddStatementResult(errorStatementResult); + break; + } + } + + return interpretResult; + } + + public Task VisitAssignmentExpression(AssignmentExpression assignmentExpression) + { + throw new NotImplementedException("To be supported on future release."); + } + + public async Task VisitBinaryExpression(BinaryExpression binaryExpression) + { + try + { + var left = await binaryExpression.LeftExpression.Accept(this).ConfigureAwait(false); + var right = await binaryExpression.RightExpression.Accept(this).ConfigureAwait(false); + var rqlOperator = (RqlOperators)await binaryExpression.OperatorSegment.Accept(this).ConfigureAwait(false); + return this.runtime.ApplyBinary(left, rqlOperator, right); + } + catch (RuntimeException ex) + { + throw CreateInterpreterException(ex.Message, binaryExpression); + } + } + + public async Task VisitCardinalitySegment(CardinalitySegment expression) => await expression.CardinalityKeyword.Accept(this).ConfigureAwait(false); + + public async Task VisitExpressionStatement(ExpressionStatement expressionStatement) + { + var rql = this.reverseRqlBuilder.BuildRql(expressionStatement); + var expressionResult = await expressionStatement.Expression.Accept(this).ConfigureAwait(false); + return new ExpressionStatementResult(rql, expressionResult); + } + + public Task VisitIdentifierExpression(IdentifierExpression identifierExpression) + => Task.FromResult(new RqlString(identifierExpression.Identifier.UnescapedLexeme)); + + public async Task VisitInputConditionSegment(InputConditionSegment inputConditionExpression) + { + var conditionTypeName = (RqlString)await inputConditionExpression.Left.Accept(this).ConfigureAwait(false); + object conditionType; + +#if NETSTANDARD2_0 + try + { + conditionType = Enum.Parse(typeof(TConditionType), conditionTypeName.Value); + } + catch (Exception) + { + throw CreateInterpreterException(new[] { FormattableString.Invariant($"Condition type of name '{conditionTypeName}' was not found.") }, inputConditionExpression); + } +#else + if (!Enum.TryParse(typeof(TConditionType), conditionTypeName.Value, out conditionType)) + { + throw CreateInterpreterException(new[] { FormattableString.Invariant($"Condition type of name '{conditionTypeName}' was not found.") }, inputConditionExpression); + } +#endif + + var conditionValue = await inputConditionExpression.Right.Accept(this).ConfigureAwait(false); + return new Condition((TConditionType)conditionType, conditionValue.RuntimeValue); + } + + public async Task VisitInputConditionsSegment(InputConditionsSegment inputConditionsExpression) + { + var inputConditions = inputConditionsExpression.InputConditions; + var inputConditionsLength = inputConditions.Length; + var conditions = new Condition[inputConditionsLength]; + for (int i = 0; i < inputConditionsLength; i++) + { + conditions[i] = (Condition)await inputConditions[i].Accept(this).ConfigureAwait(false); + } + + return conditions; + } + + public Task VisitKeywordExpression(KeywordExpression keywordExpression) + => Task.FromResult(new RqlString(keywordExpression.Keyword.Lexeme)); + + public Task VisitLiteralExpression(LiteralExpression literalExpression) + { + return Task.FromResult(literalExpression.Type switch + { + LiteralType.Bool when literalExpression.Value is null => new RqlNothing(), + LiteralType.Bool => new RqlBool((bool)literalExpression.Value), + LiteralType.Decimal when literalExpression.Value is null => new RqlNothing(), + LiteralType.Decimal => new RqlDecimal((decimal)literalExpression.Value), + LiteralType.Integer when literalExpression.Value is null => new RqlNothing(), + LiteralType.Integer => new RqlInteger((int)literalExpression.Value), + LiteralType.String when literalExpression.Value is null => new RqlNothing(), + LiteralType.String => new RqlString((string)literalExpression.Value), + LiteralType.DateTime when literalExpression.Value is null => new RqlNothing(), + LiteralType.DateTime => new RqlDate(((DateTime)literalExpression.Value).ToUniversalTime()), + LiteralType.Undefined => new RqlNothing(), + _ when literalExpression.Value is null => new RqlNothing(), + _ => throw new NotSupportedException($"Literal with type '{literalExpression.Type}' is not supported."), + }); + } + + public async Task VisitMatchExpression(MatchExpression matchExpression) + { + try + { + var cardinality = (RqlString)await matchExpression.Cardinality.Accept(this).ConfigureAwait(false); + var contentType = await this.HandleContentTypeAsync(matchExpression.ContentType).ConfigureAwait(false); + var matchDate = (RqlDate)await matchExpression.MatchDate.Accept(this).ConfigureAwait(false); + var inputConditions = await matchExpression.InputConditions.Accept(this).ConfigureAwait(false); + var conditions = inputConditions is null ? Array.Empty>() : (IEnumerable>)inputConditions; + var matchCardinality = string.Equals(cardinality.Value, "ONE", StringComparison.OrdinalIgnoreCase) + ? MatchCardinality.One + : MatchCardinality.All; + var matchRulesArgs = new MatchRulesArgs + { + Conditions = conditions, + ContentType = contentType, + MatchCardinality = matchCardinality, + MatchDate = matchDate, + }; + + return await this.runtime.MatchRulesAsync(matchRulesArgs).ConfigureAwait(false); + } + catch (RuntimeException ex) + { + throw CreateInterpreterException(ex.Message, matchExpression); + } + } + + public async Task VisitNewArrayExpression(NewArrayExpression newArrayExpression) + { + var sizeValue = await newArrayExpression.Size.Accept(this).ConfigureAwait(false); + var size = sizeValue is RqlInteger integer ? integer.Value : newArrayExpression.Values.Length; + var hasArrayInitializer = newArrayExpression.Values.Length > 0; + var rqlArray = new RqlArray(size, !hasArrayInitializer); + + if (hasArrayInitializer) + { + for (var i = 0; i < size; i++) + { + var value = await newArrayExpression.Values[i].Accept(this).ConfigureAwait(false); + rqlArray.Value[i] = new RqlAny(value); + } + } + + return rqlArray; + } + + public async Task VisitNewObjectExpression(NewObjectExpression newObjectExpression) + { + var rqlObject = new RqlObject(); + var propertyAssignments = newObjectExpression.PropertyAssignments; + for (int i = 0; i < propertyAssignments.Length; i++) + { + var assignment = (AssignmentExpression)propertyAssignments[i]; + var left = (RqlString)await assignment.Left.Accept(this).ConfigureAwait(false); + var right = await assignment.Right.Accept(this).ConfigureAwait(false); + rqlObject.SetPropertyValue(left, new RqlAny(right)); + } + + return rqlObject; + } + + public Task VisitNoneExpression(NoneExpression noneExpression) => Task.FromResult(new RqlNothing()); + + public Task VisitNoneSegment(NoneSegment noneSegment) => Task.FromResult(null!); + + public Task VisitNoneStatement(NoneStatement statement) => Task.FromResult(new ExpressionStatementResult(string.Empty, new RqlNothing())); + + public Task VisitOperatorSegment(OperatorSegment operatorExpression) + { + var resultOperator = RqlOperators.None; + switch (operatorExpression.Tokens[0].Type) + { + case TokenType.AND: + resultOperator = RqlOperators.And; + break; + + case TokenType.ASSIGN: + resultOperator = RqlOperators.Assign; + break; + + case TokenType.EQUAL: + resultOperator = RqlOperators.Equals; + break; + + case TokenType.GREATER_THAN: + resultOperator = RqlOperators.GreaterThan; + break; + + case TokenType.GREATER_THAN_OR_EQUAL: + resultOperator = RqlOperators.GreaterThanOrEquals; + break; + + case TokenType.IN: + resultOperator = RqlOperators.In; + break; + + case TokenType.LESS_THAN: + resultOperator = RqlOperators.LesserThan; + break; + + case TokenType.LESS_THAN_OR_EQUAL: + resultOperator = RqlOperators.LesserThanOrEquals; + break; + + case TokenType.MINUS: + resultOperator = RqlOperators.Minus; + break; + + case TokenType.NOT: + if (operatorExpression.Tokens.Length > 1 && operatorExpression.Tokens[1].Type == TokenType.IN) + { + resultOperator = RqlOperators.NotIn; + } + break; + + case TokenType.NOT_EQUAL: + resultOperator = RqlOperators.NotEquals; + break; + + case TokenType.OR: + resultOperator = RqlOperators.Or; + break; + + case TokenType.PLUS: + resultOperator = RqlOperators.Plus; + break; + + case TokenType.SLASH: + resultOperator = RqlOperators.Slash; + break; + + case TokenType.STAR: + resultOperator = RqlOperators.Star; + break; + } + + if (resultOperator == RqlOperators.None) + { + var tokenTypes = operatorExpression.Tokens.Select(t => $"'{t.Type}'").Aggregate((t1, t2) => $"{t1}, {t2}"); + throw new NotSupportedException($"The tokens with types [{tokenTypes}] are not supported as a valid operator."); + } + + return Task.FromResult(resultOperator); + } + + public Task VisitPlaceholderExpression(PlaceholderExpression placeholderExpression) + => Task.FromResult(new RqlString((string)placeholderExpression.Token.Literal)); + + public async Task VisitSearchExpression(SearchExpression searchExpression) + { + try + { + var contentType = await this.HandleContentTypeAsync(searchExpression.ContentType).ConfigureAwait(false); + var dateBegin = (RqlDate)await searchExpression.DateBegin.Accept(this).ConfigureAwait(false); + var dateEnd = (RqlDate)await searchExpression.DateEnd.Accept(this).ConfigureAwait(false); + var conditions = (IEnumerable>)await searchExpression.InputConditions.Accept(this).ConfigureAwait(false); + var searchRulesArgs = new SearchRulesArgs + { + Conditions = conditions ?? Enumerable.Empty>(), + ContentType = contentType, + DateBegin = dateBegin, + DateEnd = dateEnd, + }; + + return await this.runtime.SearchRulesAsync(searchRulesArgs).ConfigureAwait(false); + } + catch (RuntimeException ex) + { + throw CreateInterpreterException(ex.Message, searchExpression); + } + } + + public async Task VisitUnaryExpression(UnaryExpression unaryExpression) + { + try + { + var @operator = unaryExpression.Operator.Lexeme switch + { + "-" => RqlOperators.Minus, + _ => RqlOperators.None, + }; + var right = await unaryExpression.Right.Accept(this).ConfigureAwait(false); + return this.runtime.ApplyUnary(right, @operator); + } + catch (RuntimeException re) + { + throw CreateInterpreterException(re.Errors, unaryExpression); + } + } + + private Exception CreateInterpreterException(IEnumerable errors, IAstElement astElement) + { + var rql = this.reverseRqlBuilder.BuildRql(astElement); + var separator = $"{Environment.NewLine}\t - "; + var errorsText = string.Join(separator, errors); + return new InterpreterException( + $"Errors have occurred while executing sentence:{separator}{errorsText}", + rql, + astElement.BeginPosition, + astElement.EndPosition); + } + + private Exception CreateInterpreterException(string error, IAstElement astElement) + { + return CreateInterpreterException(new[] { error }, astElement); + } + + private async Task HandleContentTypeAsync(Expression contentTypeExpression) + { + var rawValue = await contentTypeExpression.Accept(this).ConfigureAwait(false); + var value = RqlTypes.Any.IsAssignableTo(rawValue.Type) ? ((RqlAny)rawValue).Unwrap() : rawValue; + if (!RqlTypes.String.IsAssignableTo(value.Type)) + { + throw CreateInterpreterException($"Expected a content type value of type '{RqlTypes.String.Name}' but found '{value.Type.Name}' instead", contentTypeExpression); + } + + try + { + return (TContentType)Enum.Parse(typeof(TContentType), ((RqlString)value).Value, ignoreCase: true); + } + catch (Exception) + { + throw CreateInterpreterException($"The content type value '{value.RuntimeValue}' was not found", contentTypeExpression); + } + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Interpret/InterpreterException.cs b/src/Rules.Framework.Rql/Pipeline/Interpret/InterpreterException.cs new file mode 100644 index 00000000..7f584121 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Interpret/InterpreterException.cs @@ -0,0 +1,36 @@ +namespace Rules.Framework.Rql.Pipeline.Interpret +{ + using System; + using System.Diagnostics.CodeAnalysis; + using System.Runtime.Serialization; + + [ExcludeFromCodeCoverage] + internal class InterpreterException : Exception + { + public InterpreterException( + string message, + string rql, + RqlSourcePosition beginPosition, + RqlSourcePosition endPosition) + : base(message) + { + this.Rql = rql; + this.BeginPosition = beginPosition; + this.EndPosition = endPosition; + } + + protected InterpreterException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + this.Rql = info.GetString(nameof(this.Rql)); + this.BeginPosition = (RqlSourcePosition)info.GetValue(nameof(this.BeginPosition), typeof(RqlSourcePosition)); + this.EndPosition = (RqlSourcePosition)info.GetValue(nameof(this.EndPosition), typeof(RqlSourcePosition)); + } + + public RqlSourcePosition BeginPosition { get; } + + public RqlSourcePosition EndPosition { get; } + + public string Rql { get; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Interpret/NothingStatementResult.cs b/src/Rules.Framework.Rql/Pipeline/Interpret/NothingStatementResult.cs new file mode 100644 index 00000000..ef0bd24b --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Interpret/NothingStatementResult.cs @@ -0,0 +1,24 @@ +namespace Rules.Framework.Rql.Pipeline.Interpret +{ + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + internal class NothingStatementResult : IResult + { + public NothingStatementResult(string rql) + { + if (string.IsNullOrWhiteSpace(rql)) + { + throw new System.ArgumentException($"'{nameof(rql)}' cannot be null or whitespace.", nameof(rql)); + } + + this.Rql = rql; + } + + public bool HasOutput => false; + + public string Rql { get; } + + public bool Success => true; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/IExpressionParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/IExpressionParseStrategy.cs new file mode 100644 index 00000000..7e284d74 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/IExpressionParseStrategy.cs @@ -0,0 +1,8 @@ +namespace Rules.Framework.Rql.Pipeline.Parse +{ + using Rules.Framework.Rql.Ast.Expressions; + + internal interface IExpressionParseStrategy : IParseStrategy + { + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/IParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/IParseStrategy.cs new file mode 100644 index 00000000..280c8e78 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/IParseStrategy.cs @@ -0,0 +1,7 @@ +namespace Rules.Framework.Rql.Pipeline.Parse +{ + internal interface IParseStrategy + { + TParseOutput Parse(ParseContext parseContext); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/IParseStrategyProvider.cs b/src/Rules.Framework.Rql/Pipeline/Parse/IParseStrategyProvider.cs new file mode 100644 index 00000000..ac39b8cc --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/IParseStrategyProvider.cs @@ -0,0 +1,11 @@ +namespace Rules.Framework.Rql.Pipeline.Parse +{ + internal interface IParseStrategyProvider + { + TExpressionParseStrategy GetExpressionParseStrategy() where TExpressionParseStrategy : IExpressionParseStrategy; + + TSegmentParseStrategy GetSegmentParseStrategy() where TSegmentParseStrategy : ISegmentParseStrategy; + + TStatementParseStrategy GetStatementParseStrategy() where TStatementParseStrategy : IStatementParseStrategy; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/IParser.cs b/src/Rules.Framework.Rql/Pipeline/Parse/IParser.cs new file mode 100644 index 00000000..3f69281d --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/IParser.cs @@ -0,0 +1,10 @@ +namespace Rules.Framework.Rql.Pipeline.Parse +{ + using System.Collections.Generic; + using Rules.Framework.Rql.Tokens; + + internal interface IParser + { + ParseResult Parse(IReadOnlyList tokens); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/ISegmentParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/ISegmentParseStrategy.cs new file mode 100644 index 00000000..89059ce5 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/ISegmentParseStrategy.cs @@ -0,0 +1,8 @@ +namespace Rules.Framework.Rql.Pipeline.Parse +{ + using Rules.Framework.Rql.Ast.Segments; + + internal interface ISegmentParseStrategy : IParseStrategy + { + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/IStatementParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/IStatementParseStrategy.cs new file mode 100644 index 00000000..b7d6bae2 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/IStatementParseStrategy.cs @@ -0,0 +1,8 @@ +namespace Rules.Framework.Rql.Pipeline.Parse +{ + using Rules.Framework.Rql.Ast.Statements; + + internal interface IStatementParseStrategy : IParseStrategy + { + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/PanicModeInfo.cs b/src/Rules.Framework.Rql/Pipeline/Parse/PanicModeInfo.cs new file mode 100644 index 00000000..ca75ee94 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/PanicModeInfo.cs @@ -0,0 +1,21 @@ +namespace Rules.Framework.Rql.Pipeline.Parse +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql.Tokens; + + [ExcludeFromCodeCoverage] + internal readonly struct PanicModeInfo + { + public static readonly PanicModeInfo None = new(causeToken: null!, message: null!); + + public PanicModeInfo(Token causeToken, string message) + { + this.CauseToken = causeToken; + this.Message = message; + } + + public Token CauseToken { get; } + + public string Message { get; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/ParseContext.cs b/src/Rules.Framework.Rql/Pipeline/Parse/ParseContext.cs new file mode 100644 index 00000000..7362206c --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/ParseContext.cs @@ -0,0 +1,146 @@ +namespace Rules.Framework.Rql.Pipeline.Parse +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Rules.Framework.Rql.Tokens; + + internal class ParseContext + { + public ParseContext(IReadOnlyList tokens) + { + this.PanicMode = false; + this.PanicModeInfo = PanicModeInfo.None; + this.Tokens = tokens; + this.Offset = -1; + } + + public int Offset { get; private set; } + + public bool PanicMode { get; private set; } + + public PanicModeInfo PanicModeInfo { get; private set; } + + public IReadOnlyList Tokens { get; } + + public void EnterPanicMode(string infoMessage, Token causeToken) + { + if (this.PanicMode) + { + throw new InvalidOperationException("Parse operation is already in panic mode."); + } + + this.PanicMode = true; + this.PanicModeInfo = new PanicModeInfo(causeToken, infoMessage); + } + + public void ExitPanicMode() + { + if (!this.PanicMode) + { + throw new InvalidOperationException("Parse operation is not in panic mode."); + } + + this.PanicMode = false; + this.PanicModeInfo = PanicModeInfo.None; + } + + public Token GetCurrentToken() + => this.GetToken(this.Offset); + + public Token GetNextToken() + { + if (this.Offset + 1 >= this.Tokens.Count) + { + return this.GetToken(this.Tokens.Count - 1); + } + + return this.GetToken(this.Offset + 1); + } + + public bool IsEof() => this.IsEof(this.Offset); + + public bool IsMatchAtOffsetFromCurrent(int offsetFromCurrent, params TokenType[] tokenTypes) + => this.IsMatch(this.Offset + offsetFromCurrent, tokenTypes); + + public bool IsMatchCurrentToken(params TokenType[] tokenTypes) + => this.IsMatch(this.Offset, tokenTypes); + + public bool IsMatchNextToken(params TokenType[] tokenTypes) + => this.IsMatch(this.Offset + 1, tokenTypes); + + public bool MoveNext() + => this.Move(this.Offset + 1); + + public bool MoveNextIfCurrentToken(params TokenType[] tokenTypes) + { + if (this.IsMatchCurrentToken(tokenTypes)) + { + return this.MoveNext(); + } + + return false; + } + + public bool MoveNextIfNextToken(params TokenType[] tokenTypes) + { + if (this.IsMatchNextToken(tokenTypes)) + { + return this.MoveNext(); + } + + return false; + } + + private Token GetToken(int offset) + { + if (offset < 0) + { + throw new InvalidOperationException("Must invoke MoveNext() first."); + } + + return this.Tokens[offset]; + } + + private bool IsEof(int offset) => offset >= this.Tokens.Count || this.Tokens[offset].Type == TokenType.EOF; + + private bool IsMatch(int offset, params TokenType[] tokenTypes) + { + if (offset < 0) + { + throw new ArgumentOutOfRangeException(nameof(offset), "Offset must be zero or greater."); + } + + if (this.IsEof(offset)) + { + if (tokenTypes.Contains(TokenType.EOF)) + { + return true; + } + + return false; + } + + foreach (var tokenType in tokenTypes) + { + if (this.Tokens[offset].Type == tokenType) + { + return true; + } + } + + return false; + } + + private bool Move(int toOffset) + { + if (toOffset < this.Tokens.Count) + { + this.Offset = toOffset; + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/ParseResult.cs b/src/Rules.Framework.Rql/Pipeline/Parse/ParseResult.cs new file mode 100644 index 00000000..60122a48 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/ParseResult.cs @@ -0,0 +1,28 @@ +namespace Rules.Framework.Rql.Pipeline.Parse +{ + using System.Collections.Generic; + using Rules.Framework.Rql.Ast.Statements; + using Rules.Framework.Rql.Messages; + + internal class ParseResult + { + private ParseResult(bool success, IReadOnlyList messages, IReadOnlyList statements) + { + this.Success = success; + this.Messages = messages; + this.Statements = statements; + } + + public IReadOnlyList Messages { get; } + + public IReadOnlyList Statements { get; } + + public bool Success { get; } + + public static ParseResult CreateError(IReadOnlyList messages) + => new ParseResult(success: false, messages, statements: null); + + public static ParseResult CreateSuccess(IReadOnlyList statements, IReadOnlyList messages) + => new ParseResult(success: true, messages, statements); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/ParseStrategyPool.cs b/src/Rules.Framework.Rql/Pipeline/Parse/ParseStrategyPool.cs new file mode 100644 index 00000000..02457cf1 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/ParseStrategyPool.cs @@ -0,0 +1,55 @@ +namespace Rules.Framework.Rql.Pipeline.Parse +{ + using System; + using System.Collections.Generic; + + internal class ParseStrategyPool : IParseStrategyProvider + { + private readonly Dictionary expressionParseStrategies; + private readonly Dictionary segmentParseStrategies; + private readonly Dictionary statementParseStrategies; + + public ParseStrategyPool() + { + this.expressionParseStrategies = new Dictionary(); + this.segmentParseStrategies = new Dictionary(); + this.statementParseStrategies = new Dictionary(); + } + + public TExpressionParseStrategy GetExpressionParseStrategy() where TExpressionParseStrategy : IExpressionParseStrategy + { + if (this.expressionParseStrategies.TryGetValue(typeof(TExpressionParseStrategy), out var expressionParseStrategy)) + { + return (TExpressionParseStrategy)expressionParseStrategy; + } + + expressionParseStrategy = (TExpressionParseStrategy)Activator.CreateInstance(typeof(TExpressionParseStrategy), this); + this.expressionParseStrategies[typeof(TExpressionParseStrategy)] = expressionParseStrategy; + return (TExpressionParseStrategy)expressionParseStrategy; + } + + public TSegmentParseStrategy GetSegmentParseStrategy() where TSegmentParseStrategy : ISegmentParseStrategy + { + if (this.segmentParseStrategies.TryGetValue(typeof(TSegmentParseStrategy), out var segmentParseStrategy)) + { + return (TSegmentParseStrategy)segmentParseStrategy; + } + + segmentParseStrategy = (TSegmentParseStrategy)Activator.CreateInstance(typeof(TSegmentParseStrategy), this); + this.segmentParseStrategies[typeof(TSegmentParseStrategy)] = segmentParseStrategy; + return (TSegmentParseStrategy)segmentParseStrategy; + } + + public TStatementParseStrategy GetStatementParseStrategy() where TStatementParseStrategy : IStatementParseStrategy + { + if (this.statementParseStrategies.TryGetValue(typeof(TStatementParseStrategy), out var statementParseStrategy)) + { + return (TStatementParseStrategy)statementParseStrategy; + } + + statementParseStrategy = (TStatementParseStrategy)Activator.CreateInstance(typeof(TStatementParseStrategy), this); + this.statementParseStrategies[typeof(TStatementParseStrategy)] = statementParseStrategy; + return (TStatementParseStrategy)statementParseStrategy; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Parser.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Parser.cs new file mode 100644 index 00000000..e131268f --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Parser.cs @@ -0,0 +1,68 @@ +namespace Rules.Framework.Rql.Pipeline.Parse +{ + using System.Collections.Generic; + using Rules.Framework.Rql.Ast.Statements; + using Rules.Framework.Rql.Messages; + using Rules.Framework.Rql.Pipeline.Parse.Strategies; + using Rules.Framework.Rql.Tokens; + + internal class Parser : IParser + { + private readonly IParseStrategyProvider parseStrategyProvider; + + public Parser(IParseStrategyProvider parseStrategyProvider) + { + this.parseStrategyProvider = parseStrategyProvider; + } + + public ParseResult Parse(IReadOnlyList tokens) + { + var parseContext = new ParseContext(tokens); + var statements = new List(); + + using var messageContainer = new MessageContainer(); + while (parseContext.MoveNext()) + { + var statement = this.parseStrategyProvider.GetStatementParseStrategy().Parse(parseContext); + if (parseContext.PanicMode) + { + var panicModeInfo = parseContext.PanicModeInfo; + messageContainer.Error( + panicModeInfo.Message, + panicModeInfo.CauseToken.BeginPosition, + panicModeInfo.CauseToken.EndPosition); + Synchronize(parseContext); + parseContext.ExitPanicMode(); + } + else + { + statements.Add(statement); + } + } + + var messages = messageContainer.Messages; + if (messageContainer.ErrorsCount > 0) + { + return ParseResult.CreateError(messages); + } + + return ParseResult.CreateSuccess(statements, messages); + } + + private static void Synchronize(ParseContext parseContext) + { + while (parseContext.MoveNext()) + { + switch (parseContext.GetCurrentToken().Type) + { + case TokenType.SEMICOLON: + case TokenType.EOF: + return; + + default: + break; + } + } + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ArrayParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ArrayParseStrategy.cs new file mode 100644 index 00000000..9f48f2a2 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ArrayParseStrategy.cs @@ -0,0 +1,106 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using System; + using System.Collections.Generic; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Tokens; + + internal class ArrayParseStrategy : ParseStrategyBase, IExpressionParseStrategy + { + public ArrayParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Expression Parse(ParseContext parseContext) + { + if (!parseContext.IsMatchCurrentToken(TokenType.ARRAY, TokenType.BRACE_LEFT)) + { + throw new InvalidOperationException("Unable to handle array expression."); + } + + Token initializerBeginToken; + Token initializerEndToken; + if (parseContext.IsMatchCurrentToken(TokenType.BRACE_LEFT)) + { + initializerBeginToken = parseContext.GetCurrentToken(); + _ = parseContext.MoveNext(); + + // TODO: update according to future logic to process 'or' expressions. + var literal = this.ParseExpressionWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + _ = parseContext.MoveNext(); + var values = new List { literal }; + while (parseContext.IsMatchCurrentToken(TokenType.COMMA)) + { + _ = parseContext.MoveNext(); + + // TODO: update according to future logic to process 'or' expressions. + literal = this.ParseExpressionWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + values.Add(literal); + _ = parseContext.MoveNext(); + } + + if (!parseContext.IsMatchCurrentToken(TokenType.BRACE_RIGHT)) + { + parseContext.EnterPanicMode("Expected token '}'.", parseContext.GetCurrentToken()); + return Expression.None; + } + + initializerEndToken = parseContext.GetCurrentToken(); + return NewArrayExpression.Create(Token.None, initializerBeginToken, Expression.None, values.ToArray(), initializerEndToken); + } + + // At this moment, assumes that an empty with fixed size is being declared. + var arrayToken = parseContext.GetCurrentToken(); + if (!parseContext.MoveNextIfNextToken(TokenType.STRAIGHT_BRACKET_LEFT)) + { + parseContext.EnterPanicMode("Expected token '['.", parseContext.GetNextToken()); + return Expression.None; + } + + initializerBeginToken = parseContext.GetCurrentToken(); + _ = parseContext.MoveNext(); + var size = this.ParseSizeExpression(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + if (!parseContext.MoveNextIfNextToken(TokenType.STRAIGHT_BRACKET_RIGHT)) + { + parseContext.EnterPanicMode("Expected token ']'.", parseContext.GetNextToken()); + return Expression.None; + } + + initializerEndToken = parseContext.GetCurrentToken(); + return NewArrayExpression.Create(arrayToken, initializerBeginToken, size, Array.Empty(), initializerEndToken); + } + + private Expression ParseSizeExpression(ParseContext parseContext) + { + if (!parseContext.IsMatchCurrentToken(TokenType.INT)) + { + parseContext.EnterPanicMode("Expected integer literal.", parseContext.GetCurrentToken()); + return Expression.None; + } + + var literal = this.ParseExpressionWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + return literal; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/AssignmentParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/AssignmentParseStrategy.cs new file mode 100644 index 00000000..31485d13 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/AssignmentParseStrategy.cs @@ -0,0 +1,20 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Tokens; + + internal class AssignmentParseStrategy : ParseStrategyBase, IExpressionParseStrategy + { + public AssignmentParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Expression Parse(ParseContext parseContext) + { + return this.ParseExpressionWith(parseContext); + + // TODO: future logic to be added here for dealing with assignment of variables. + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/BaseExpressionParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/BaseExpressionParseStrategy.cs new file mode 100644 index 00000000..c82135f4 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/BaseExpressionParseStrategy.cs @@ -0,0 +1,46 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Tokens; + + internal class BaseExpressionParseStrategy : ParseStrategyBase, IExpressionParseStrategy + { + public BaseExpressionParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Expression Parse(ParseContext parseContext) + { + var currentToken = parseContext.GetCurrentToken(); + if (parseContext.IsMatchCurrentToken(Constants.AllowedUnescapedIdentifierNames) || (currentToken.IsEscaped && !parseContext.IsMatchCurrentToken(Constants.AllowedEscapedIdentifierNames))) + { + // TODO: logic to be changed to flow first through a indexer parse rule. + return this.ParseExpressionWith(parseContext); + } + + if (parseContext.IsMatchCurrentToken(TokenType.ARRAY, TokenType.BRACE_LEFT)) + { + return this.ParseExpressionWith(parseContext); + } + + if (parseContext.IsMatchCurrentToken(TokenType.OBJECT)) + { + return this.ParseExpressionWith(parseContext); + } + + if (parseContext.IsMatchCurrentToken(TokenType.NOTHING)) + { + return this.ParseExpressionWith(parseContext); + } + + if (parseContext.IsMatchCurrentToken(TokenType.STRING, TokenType.INT, TokenType.BOOL, TokenType.DECIMAL, TokenType.DATE)) + { + return this.ParseExpressionWith(parseContext); + } + + parseContext.EnterPanicMode("Expected expression.", currentToken); + return Expression.None; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/CardinalityParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/CardinalityParseStrategy.cs new file mode 100644 index 00000000..e617c008 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/CardinalityParseStrategy.cs @@ -0,0 +1,47 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using Rules.Framework.Rql.Ast.Segments; + using Rules.Framework.Rql.Tokens; + + internal class CardinalityParseStrategy : ParseStrategyBase, ISegmentParseStrategy + { + public CardinalityParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Segment Parse(ParseContext parseContext) + { + if (parseContext.IsMatchCurrentToken(TokenType.ONE)) + { + var oneCardinalityKeyword = this.ParseExpressionWith(parseContext); + if (!parseContext.MoveNextIfNextToken(TokenType.RULE)) + { + parseContext.EnterPanicMode("Expected token 'RULE'.", parseContext.GetNextToken()); + return Segment.None; + } + + var ruleKeyword = this.ParseExpressionWith(parseContext); + + return CardinalitySegment.Create(oneCardinalityKeyword, ruleKeyword); + } + + if (parseContext.IsMatchCurrentToken(TokenType.ALL)) + { + var allCardinalityKeyword = this.ParseExpressionWith(parseContext); + if (!parseContext.MoveNextIfNextToken(TokenType.RULES)) + { + parseContext.EnterPanicMode("Expected token 'RULES'.", parseContext.GetNextToken()); + return Segment.None; + } + + var ruleKeyword = this.ParseExpressionWith(parseContext); + + return CardinalitySegment.Create(allCardinalityKeyword, ruleKeyword); + } + + parseContext.EnterPanicMode("Expected tokens 'ONE' or 'ALL'.", parseContext.GetCurrentToken()); + return Segment.None; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ContentTypeParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ContentTypeParseStrategy.cs new file mode 100644 index 00000000..fbe3baa3 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ContentTypeParseStrategy.cs @@ -0,0 +1,47 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using System; + using System.Linq; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Tokens; + + internal class ContentTypeParseStrategy : ParseStrategyBase, IExpressionParseStrategy + { + private static readonly LiteralType[] allowedLiteralTypesAsContentType = new[] { LiteralType.Integer, LiteralType.String }; + + private static readonly Lazy allowedLiteralTypesMessage = new(() => + $"Only literals of types [{allowedLiteralTypesAsContentType.Select(t => t.ToString()).Aggregate((t1, t2) => $"{t1}, {t2}")}] are allowed."); + + public ContentTypeParseStrategy(IParseStrategyProvider parseStrategyProvider) : base(parseStrategyProvider) + { + } + + public override Expression Parse(ParseContext parseContext) + { + if (!parseContext.IsMatchCurrentToken(TokenType.FOR)) + { + throw new InvalidOperationException("Unable to handle content type expression."); + } + + if (!parseContext.MoveNext()) + { + parseContext.EnterPanicMode("Expected content type name.", parseContext.GetNextToken()); + return Expression.None; + } + + var contentExpression = this.ParseExpressionWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + if (contentExpression is LiteralExpression literalExpression && !allowedLiteralTypesAsContentType.Contains(literalExpression.Type)) + { + parseContext.EnterPanicMode($"Literal '{literalExpression.Token.Lexeme}' is not allowed as a valid content type. {allowedLiteralTypesMessage.Value}", literalExpression.Token); + return Expression.None; + } + + return contentExpression; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/DeclarationParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/DeclarationParseStrategy.cs new file mode 100644 index 00000000..38c234d6 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/DeclarationParseStrategy.cs @@ -0,0 +1,20 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using Rules.Framework.Rql.Ast.Statements; + using Rules.Framework.Rql.Tokens; + + internal class DeclarationParseStrategy : ParseStrategyBase, IStatementParseStrategy + { + public DeclarationParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Statement Parse(ParseContext parseContext) + { + // TODO: future logic to be added here for dealing with variables. + + return this.ParseStatementWith(parseContext); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ExpressionParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ExpressionParseStrategy.cs new file mode 100644 index 00000000..0188f834 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ExpressionParseStrategy.cs @@ -0,0 +1,17 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using Rules.Framework.Rql.Ast.Expressions; + + internal class ExpressionParseStrategy : ParseStrategyBase, IExpressionParseStrategy + { + public ExpressionParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Expression Parse(ParseContext parseContext) + { + return this.ParseExpressionWith(parseContext); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ExpressionStatementParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ExpressionStatementParseStrategy.cs new file mode 100644 index 00000000..c34b2f9a --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ExpressionStatementParseStrategy.cs @@ -0,0 +1,30 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using Rules.Framework.Rql.Ast.Statements; + using Rules.Framework.Rql.Tokens; + + internal class ExpressionStatementParseStrategy : ParseStrategyBase, IStatementParseStrategy + { + public ExpressionStatementParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Statement Parse(ParseContext parseContext) + { + var expression = this.ParseExpressionWith(parseContext); + if (parseContext.PanicMode) + { + return Statement.None; + } + + if (!parseContext.MoveNextIfNextToken(TokenType.SEMICOLON)) + { + parseContext.EnterPanicMode("Expected token ';'.", parseContext.GetNextToken()); + return Statement.None; + } + + return ExpressionStatement.Create(expression); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/FactorParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/FactorParseStrategy.cs new file mode 100644 index 00000000..f691d92c --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/FactorParseStrategy.cs @@ -0,0 +1,42 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Tokens; + + internal class FactorParseStrategy : ParseStrategyBase, IExpressionParseStrategy + { + public FactorParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Expression Parse(ParseContext parseContext) + { + var unaryExpression = this.ParseExpressionWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + if (parseContext.MoveNextIfNextToken(TokenType.SLASH, TokenType.STAR)) + { + var operatorSegment = this.ParseSegmentWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + _ = parseContext.MoveNext(); + var rightExpression = this.ParseExpressionWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + return new BinaryExpression(unaryExpression, operatorSegment, rightExpression); + } + + return unaryExpression; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/IdentifierParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/IdentifierParseStrategy.cs new file mode 100644 index 00000000..20871e86 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/IdentifierParseStrategy.cs @@ -0,0 +1,18 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using Rules.Framework.Rql.Ast.Expressions; + + internal class IdentifierParseStrategy : ParseStrategyBase, IExpressionParseStrategy + { + public IdentifierParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Expression Parse(ParseContext parseContext) + { + var identifierToken = parseContext.GetCurrentToken(); + return new IdentifierExpression(identifierToken); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/InputConditionParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/InputConditionParseStrategy.cs new file mode 100644 index 00000000..22bdbe10 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/InputConditionParseStrategy.cs @@ -0,0 +1,48 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using System; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Ast.Segments; + using Rules.Framework.Rql.Tokens; + + internal class InputConditionParseStrategy : ParseStrategyBase, ISegmentParseStrategy + { + public InputConditionParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Segment Parse(ParseContext parseContext) + { + if (!parseContext.IsMatchCurrentToken(TokenType.PLACEHOLDER)) + { + throw new InvalidOperationException("Unable to handle input condition expression."); + } + + var leftToken = parseContext.GetCurrentToken(); + var leftExpression = new PlaceholderExpression(leftToken); + + if (!parseContext.MoveNextIfNextToken(TokenType.IS)) + { + parseContext.EnterPanicMode("Expected token 'IS'.", parseContext.GetCurrentToken()); + return Segment.None; + } + + var operatorToken = parseContext.GetCurrentToken(); + + if (parseContext.MoveNextIfNextToken(TokenType.STRING, TokenType.INT, TokenType.DECIMAL, TokenType.BOOL, TokenType.IDENTIFIER)) + { + var rightExpression = this.ParseExpressionWith(parseContext); + if (parseContext.PanicMode) + { + return Segment.None; + } + + return new InputConditionSegment(leftExpression, operatorToken, rightExpression); + } + + parseContext.EnterPanicMode("Expected literal for condition.", parseContext.GetNextToken()); + return Segment.None; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/InputConditionsParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/InputConditionsParseStrategy.cs new file mode 100644 index 00000000..033a186e --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/InputConditionsParseStrategy.cs @@ -0,0 +1,66 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using System; + using System.Collections.Generic; + using Rules.Framework.Rql.Ast.Segments; + using Rules.Framework.Rql.Tokens; + + internal class InputConditionsParseStrategy : ParseStrategyBase, ISegmentParseStrategy + { + public InputConditionsParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Segment Parse(ParseContext parseContext) + { + if (!parseContext.MoveNextIfCurrentToken(TokenType.WHEN)) + { + throw new InvalidOperationException("Unable to handle input conditions expression."); + } + + if (!parseContext.IsMatchCurrentToken(TokenType.BRACE_LEFT)) + { + parseContext.EnterPanicMode("Expected '{' after WITH.", parseContext.GetCurrentToken()); + return Segment.None; + } + + var inputConditionExpression = this.ParseInputCondition(parseContext); + if (parseContext.PanicMode) + { + return Segment.None; + } + + var inputConditionExpressions = new List { inputConditionExpression }; + while (parseContext.MoveNextIfNextToken(TokenType.COMMA)) + { + inputConditionExpression = this.ParseInputCondition(parseContext); + if (parseContext.PanicMode) + { + return Segment.None; + } + + inputConditionExpressions.Add(inputConditionExpression); + } + + if (!parseContext.MoveNextIfNextToken(TokenType.BRACE_RIGHT)) + { + parseContext.EnterPanicMode("Expected ',' or '}' after input condition.", parseContext.GetNextToken()); + return Segment.None; + } + + return new InputConditionsSegment(inputConditionExpressions.ToArray()); + } + + private Segment ParseInputCondition(ParseContext parseContext) + { + if (parseContext.MoveNextIfNextToken(TokenType.PLACEHOLDER)) + { + return this.ParseSegmentWith(parseContext); + } + + parseContext.EnterPanicMode("Expected placeholder (@) for condition.", parseContext.GetNextToken()); + return Segment.None; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/KeywordParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/KeywordParseStrategy.cs new file mode 100644 index 00000000..aa0a37c7 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/KeywordParseStrategy.cs @@ -0,0 +1,18 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using Rules.Framework.Rql.Ast.Expressions; + + internal class KeywordParseStrategy : ParseStrategyBase, IExpressionParseStrategy + { + public KeywordParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Expression Parse(ParseContext parseContext) + { + var keywordToken = parseContext.GetCurrentToken(); + return KeywordExpression.Create(keywordToken); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/LiteralParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/LiteralParseStrategy.cs new file mode 100644 index 00000000..ffe0612f --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/LiteralParseStrategy.cs @@ -0,0 +1,41 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using System; + using System.Globalization; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Tokens; + + internal class LiteralParseStrategy : ParseStrategyBase, IExpressionParseStrategy + { + public LiteralParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Expression Parse(ParseContext parseContext) + { + var literalToken = parseContext.GetCurrentToken(); + if (literalToken.Type == TokenType.DATE) + { + if (!DateTime.TryParse((string)literalToken.Literal, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal, out var dateTimeLiteral)) + { + parseContext.EnterPanicMode("Expected date token.", literalToken); + return Expression.None; + } + + return LiteralExpression.Create(LiteralType.DateTime, literalToken, dateTimeLiteral); + } + + var inferredLiteralType = literalToken.Type switch + { + TokenType.BOOL => LiteralType.Bool, + TokenType.DECIMAL => LiteralType.Decimal, + TokenType.INT => LiteralType.Integer, + TokenType.NOTHING => LiteralType.Undefined, + TokenType.STRING => LiteralType.String, + _ => throw new NotSupportedException($"The token type '{literalToken.Type}' is not supported as a valid literal type."), + }; + return LiteralExpression.Create(inferredLiteralType, literalToken, literalToken.Literal); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/MatchRulesParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/MatchRulesParseStrategy.cs new file mode 100644 index 00000000..b39eea11 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/MatchRulesParseStrategy.cs @@ -0,0 +1,100 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using System; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Ast.Segments; + using Rules.Framework.Rql.Tokens; + + internal class MatchRulesParseStrategy : ParseStrategyBase, IExpressionParseStrategy + { + public MatchRulesParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Expression Parse(ParseContext parseContext) + { + if (!parseContext.IsMatchCurrentToken(TokenType.MATCH)) + { + throw new InvalidOperationException("Unable to handle match rules expression."); + } + + _ = parseContext.MoveNext(); + var cardinality = this.ParseSegmentWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + if (!parseContext.MoveNextIfNextToken(TokenType.FOR)) + { + parseContext.EnterPanicMode("Expected token 'FOR'.", parseContext.GetNextToken()); + return Expression.None; + } + + var contentType = this.ParseExpressionWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + var matchDate = this.ParseDate(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + Segment inputConditionsExpression; + if (parseContext.MoveNextIfNextToken(TokenType.WHEN)) + { + inputConditionsExpression = this.ParseSegmentWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + } + else + { + if (!parseContext.IsMatchNextToken(TokenType.SEMICOLON, TokenType.EOF)) + { + var token = parseContext.GetNextToken(); + parseContext.EnterPanicMode($"Unrecognized token '{token.Lexeme}'.", token); + return Expression.None; + } + + inputConditionsExpression = Segment.None; + } + + return MatchExpression.Create(cardinality, contentType, matchDate, inputConditionsExpression); + } + + private Expression ParseDate(ParseContext parseContext) + { + if (!parseContext.MoveNextIfNextToken(TokenType.ON)) + { + parseContext.EnterPanicMode("Expected token 'ON'.", parseContext.GetNextToken()); + return Expression.None; + } + + if (!parseContext.MoveNext()) + { + parseContext.EnterPanicMode("Expected literal of type date.", parseContext.GetCurrentToken()); + return Expression.None; + } + + var matchDate = this.ParseExpressionWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + if (matchDate is LiteralExpression literalExpression && literalExpression.Type != LiteralType.DateTime) + { + parseContext.EnterPanicMode("Expected literal of type date.", parseContext.GetCurrentToken()); + return Expression.None; + } + + return matchDate; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/NothingParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/NothingParseStrategy.cs new file mode 100644 index 00000000..394751e1 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/NothingParseStrategy.cs @@ -0,0 +1,24 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using System; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Tokens; + + internal class NothingParseStrategy : ParseStrategyBase, IExpressionParseStrategy + { + public NothingParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Expression Parse(ParseContext parseContext) + { + if (!parseContext.IsMatchCurrentToken(TokenType.NOTHING)) + { + throw new InvalidOperationException("Unable to handle nothing expression."); + } + + return this.ParseExpressionWith(parseContext); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ObjectParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ObjectParseStrategy.cs new file mode 100644 index 00000000..a0d73b46 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ObjectParseStrategy.cs @@ -0,0 +1,89 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using System; + using System.Collections.Generic; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Tokens; + + internal class ObjectParseStrategy : ParseStrategyBase, IExpressionParseStrategy + { + public ObjectParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Expression Parse(ParseContext parseContext) + { + if (!parseContext.IsMatchCurrentToken(TokenType.OBJECT)) + { + throw new InvalidOperationException("Unable to handle object expression."); + } + + var objectToken = parseContext.GetCurrentToken(); + if (parseContext.MoveNextIfNextToken(TokenType.BRACE_LEFT)) + { + _ = parseContext.MoveNext(); + var objectAssignment = this.ParseObjectAssignment(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + var objectAssignments = new List { objectAssignment }; + while (parseContext.MoveNextIfNextToken(TokenType.COMMA)) + { + _ = parseContext.MoveNext(); + objectAssignment = this.ParseObjectAssignment(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + objectAssignments.Add(objectAssignment); + } + + if (!parseContext.MoveNextIfNextToken(TokenType.BRACE_RIGHT)) + { + parseContext.EnterPanicMode("Expected token '}'.", parseContext.GetNextToken()); + return Expression.None; + } + + return new NewObjectExpression(objectToken, objectAssignments.ToArray()); + } + + return new NewObjectExpression(objectToken, Array.Empty()); + } + + private Expression ParseObjectAssignment(ParseContext parseContext) + { + if (!parseContext.IsMatchCurrentToken(Constants.AllowedUnescapedIdentifierNames)) + { + var currentToken = parseContext.GetCurrentToken(); + if (!currentToken.IsEscaped || !parseContext.IsMatchCurrentToken(Constants.AllowedEscapedIdentifierNames)) + { + parseContext.EnterPanicMode("Expected identifier for object property.", currentToken); + return Expression.None; + } + } + + var left = this.ParseExpressionWith(parseContext); + if (!parseContext.MoveNextIfNextToken(TokenType.ASSIGN)) + { + parseContext.EnterPanicMode("Expected token '='.", parseContext.GetNextToken()); + return Expression.None; + } + + var assign = parseContext.GetCurrentToken(); + _ = parseContext.MoveNext(); + + // TODO: update according to future logic to process 'or' expressions. + var right = this.ParseExpressionWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + return new AssignmentExpression(left, assign, right); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/OperatorParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/OperatorParseStrategy.cs new file mode 100644 index 00000000..a58d0d49 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/OperatorParseStrategy.cs @@ -0,0 +1,58 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using System; + using System.Collections.Generic; + using Rules.Framework.Rql.Ast.Segments; + using Rules.Framework.Rql.Tokens; + + internal class OperatorParseStrategy : ParseStrategyBase, ISegmentParseStrategy + { + public OperatorParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Segment Parse(ParseContext parseContext) + { + var currentToken = parseContext.GetCurrentToken(); + var operatorTokens = new List(2) + { + currentToken, + }; + + switch (currentToken.Type) + { + case TokenType.AND: + case TokenType.ASSIGN: + case TokenType.EQUAL: + case TokenType.GREATER_THAN: + case TokenType.GREATER_THAN_OR_EQUAL: + case TokenType.IN: + case TokenType.LESS_THAN: + case TokenType.LESS_THAN_OR_EQUAL: + case TokenType.MINUS: + case TokenType.NOT_EQUAL: + case TokenType.OR: + case TokenType.PLUS: + case TokenType.SLASH: + case TokenType.STAR: + break; + + case TokenType.NOT: + if (!parseContext.MoveNextIfNextToken(TokenType.IN)) + { + parseContext.EnterPanicMode("Expected token 'in'.", parseContext.GetNextToken()); + return Segment.None; + } + + operatorTokens.Add(parseContext.GetCurrentToken()); + break; + + default: + throw new InvalidOperationException("Unable to handle operator expression."); + } + + return new OperatorSegment(operatorTokens.ToArray()); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ParseStrategyBase.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ParseStrategyBase.cs new file mode 100644 index 00000000..20f2a4ea --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/ParseStrategyBase.cs @@ -0,0 +1,27 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Ast.Segments; + using Rules.Framework.Rql.Ast.Statements; + + internal abstract class ParseStrategyBase : IParseStrategy + { + private readonly IParseStrategyProvider parseStrategyProvider; + + protected ParseStrategyBase(IParseStrategyProvider parseStrategyProvider) + { + this.parseStrategyProvider = parseStrategyProvider; + } + + public abstract TParseOutput Parse(ParseContext parseContext); + + protected Expression ParseExpressionWith(ParseContext parseContext) where TExpressionParseStrategy : IExpressionParseStrategy + => this.parseStrategyProvider.GetExpressionParseStrategy().Parse(parseContext); + + protected Segment ParseSegmentWith(ParseContext parseContext) where TSegmentParseStrategy : ISegmentParseStrategy + => this.parseStrategyProvider.GetSegmentParseStrategy().Parse(parseContext); + + protected Statement ParseStatementWith(ParseContext parseContext) where TStatementParseStrategy : IStatementParseStrategy + => this.parseStrategyProvider.GetStatementParseStrategy().Parse(parseContext); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/RulesManipulationParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/RulesManipulationParseStrategy.cs new file mode 100644 index 00000000..762431d0 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/RulesManipulationParseStrategy.cs @@ -0,0 +1,30 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Tokens; + + internal class RulesManipulationParseStrategy : ParseStrategyBase, IExpressionParseStrategy + { + public RulesManipulationParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Expression Parse(ParseContext parseContext) + { + // TODO: future logic to be added here for dealing with create, update, activate, and deactivate rules. + if (parseContext.IsMatchCurrentToken(TokenType.MATCH)) + { + return this.ParseExpressionWith(parseContext); + } + + if (parseContext.IsMatchCurrentToken(TokenType.SEARCH)) + { + return this.ParseExpressionWith(parseContext); + } + + // TODO: update according to future logic to process 'or' expressions. + return this.ParseExpressionWith(parseContext); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/SearchRulesParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/SearchRulesParseStrategy.cs new file mode 100644 index 00000000..47e551d6 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/SearchRulesParseStrategy.cs @@ -0,0 +1,112 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using System; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Ast.Segments; + using Rules.Framework.Rql.Tokens; + + internal class SearchRulesParseStrategy : ParseStrategyBase, IExpressionParseStrategy + { + public SearchRulesParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Expression Parse(ParseContext parseContext) + { + if (!parseContext.IsMatchCurrentToken(TokenType.SEARCH)) + { + throw new InvalidOperationException("Unable to handle search rules expression."); + } + + if (!parseContext.MoveNextIfNextToken(TokenType.RULES)) + { + parseContext.EnterPanicMode($"Expected token '{nameof(TokenType.RULES)}'.", parseContext.GetCurrentToken()); + return Expression.None; + } + + if (!parseContext.MoveNextIfNextToken(TokenType.FOR)) + { + parseContext.EnterPanicMode($"Expected token '{nameof(TokenType.FOR)}'.", parseContext.GetCurrentToken()); + return Expression.None; + } + + var contentType = this.ParseExpressionWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + if (!parseContext.MoveNextIfNextToken(TokenType.SINCE)) + { + parseContext.EnterPanicMode($"Expected token '{nameof(TokenType.SINCE)}'.", parseContext.GetNextToken()); + return Expression.None; + } + + if (!parseContext.MoveNext()) + { + parseContext.EnterPanicMode("Expected literal of type date.", parseContext.GetCurrentToken()); + return Expression.None; + } + + var dateBegin = this.ParseExpressionWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + if (dateBegin is LiteralExpression dateBeginLiteralExpression && dateBeginLiteralExpression.Type != LiteralType.DateTime) + { + parseContext.EnterPanicMode("Expected literal of type date.", dateBeginLiteralExpression.Token); + return Expression.None; + } + + if (!parseContext.MoveNextIfNextToken(TokenType.UNTIL)) + { + parseContext.EnterPanicMode($"Expected token '{nameof(TokenType.UNTIL)}'.", parseContext.GetNextToken()); + return Expression.None; + } + + if (!parseContext.MoveNext()) + { + parseContext.EnterPanicMode("Expected literal of type date.", parseContext.GetCurrentToken()); + return Expression.None; + } + + var dateEnd = this.ParseExpressionWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + if (dateEnd is LiteralExpression dateEndLiteralExpression && dateEndLiteralExpression.Type != LiteralType.DateTime) + { + parseContext.EnterPanicMode("Expected literal of type date.", dateEndLiteralExpression.Token); + return Expression.None; + } + + Segment inputConditionsExpression; + if (parseContext.MoveNextIfNextToken(TokenType.WHEN)) + { + inputConditionsExpression = this.ParseSegmentWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + } + else + { + if (!parseContext.IsMatchNextToken(TokenType.SEMICOLON, TokenType.EOF)) + { + var token = parseContext.GetNextToken(); + parseContext.EnterPanicMode($"Unrecognized token '{token.Lexeme}'.", token); + return Expression.None; + } + + inputConditionsExpression = Segment.None; + } + + return new SearchExpression(contentType, dateBegin, dateEnd, inputConditionsExpression); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/StatementParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/StatementParseStrategy.cs new file mode 100644 index 00000000..86250643 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/StatementParseStrategy.cs @@ -0,0 +1,20 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using Rules.Framework.Rql.Ast.Statements; + using Rules.Framework.Rql.Tokens; + + internal class StatementParseStrategy : ParseStrategyBase, IStatementParseStrategy + { + public StatementParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Statement Parse(ParseContext parseContext) + { + // TODO: future logic to be added here for dealing with if, foreach, and block statements. + + return this.ParseStatementWith(parseContext); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/TermParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/TermParseStrategy.cs new file mode 100644 index 00000000..24d0dcd1 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/TermParseStrategy.cs @@ -0,0 +1,41 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Tokens; + + internal class TermParseStrategy : ParseStrategyBase, IExpressionParseStrategy + { + public TermParseStrategy(IParseStrategyProvider parseStrategyProvider) : base(parseStrategyProvider) + { + } + + public override Expression Parse(ParseContext parseContext) + { + var unaryExpression = this.ParseExpressionWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + if (parseContext.MoveNextIfNextToken(TokenType.PLUS, TokenType.MINUS)) + { + var operatorSegment = this.ParseSegmentWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + _ = parseContext.MoveNext(); + var rightExpression = this.ParseExpressionWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + return new BinaryExpression(unaryExpression, operatorSegment, rightExpression); + } + + return unaryExpression; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/UnaryParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/UnaryParseStrategy.cs new file mode 100644 index 00000000..20a6cfe5 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Strategies/UnaryParseStrategy.cs @@ -0,0 +1,31 @@ +namespace Rules.Framework.Rql.Pipeline.Parse.Strategies +{ + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Tokens; + + internal class UnaryParseStrategy : ParseStrategyBase, IExpressionParseStrategy + { + public UnaryParseStrategy(IParseStrategyProvider parseStrategyProvider) + : base(parseStrategyProvider) + { + } + + public override Expression Parse(ParseContext parseContext) + { + if (parseContext.IsMatchCurrentToken(TokenType.MINUS)) + { + var @operator = parseContext.GetCurrentToken(); + _ = parseContext.MoveNext(); + var right = this.ParseExpressionWith(parseContext); + if (parseContext.PanicMode) + { + return Expression.None; + } + + return new UnaryExpression(@operator, right); + } + + return this.ParseExpressionWith(parseContext); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Scan/IScanner.cs b/src/Rules.Framework.Rql/Pipeline/Scan/IScanner.cs new file mode 100644 index 00000000..bcd93a8a --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Scan/IScanner.cs @@ -0,0 +1,7 @@ +namespace Rules.Framework.Rql.Pipeline.Scan +{ + internal interface IScanner + { + ScanResult ScanTokens(string source); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Scan/ScanContext.cs b/src/Rules.Framework.Rql/Pipeline/Scan/ScanContext.cs new file mode 100644 index 00000000..bbb1f400 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Scan/ScanContext.cs @@ -0,0 +1,149 @@ +namespace Rules.Framework.Rql.Pipeline.Scan +{ + using System; + + internal class ScanContext + { + private readonly string source; + private int sourceColumn; + private int sourceLine; + + public ScanContext(string source) + { + this.Offset = 0; + this.sourceColumn = 0; + this.sourceLine = 1; + this.source = source; + } + + public int Offset { get; private set; } + + public TokenCandidateInfo TokenCandidate { get; private set; } + + public IDisposable BeginTokenCandidate() + { + if (this.TokenCandidate is not null) + { + throw new InvalidOperationException("A token candidate is currently created. Cannot begin a new one."); + } + + this.TokenCandidate = new TokenCandidateInfo((uint)this.Offset, (uint)this.sourceLine, (uint)this.sourceColumn); + + return new TokenCandidateScope(this); + } + + public string ExtractLexeme() + { + if (this.TokenCandidate is null) + { + throw new InvalidOperationException("Must be on a token candidate scope. Ensure you have invoked" + + $" {nameof(BeginTokenCandidate)}() and extract lexeme before disposing of token candidate."); + } + + return this.source.Substring((int)this.TokenCandidate.StartOffset, (int)(this.TokenCandidate.EndOffset - this.TokenCandidate.StartOffset + 1)); + } + + public char GetCurrentChar() + { + return this.source[this.Offset]; + } + + public char GetNextChar() + { + int nextOffset = this.Offset + 1; + if (nextOffset >= this.source.Length) + { + return '\0'; + } + + return this.source[nextOffset]; + } + + public bool IsEof() => this.Offset >= this.source.Length - 1; + + public bool MoveNext() + => this.Move(this.Offset + 1); + + public bool MoveNextConditionally(char expected) + { + var nextOffset = this.Offset + 1; + + if (nextOffset >= this.source.Length) + { + return false; + } + + if (this.source[nextOffset] != expected) + { + return false; + } + + return this.Move(nextOffset); + } + + private void DiscardTokenCandidate() + { + this.TokenCandidate = null; + } + + private bool Move(int toOffset) + { + if (toOffset >= 0 && toOffset < this.source.Length) + { + var toChar = this.source[toOffset]; + if (toChar == '\n') + { + this.NextLine(); + } + else + { + this.NextColumn(); + } + + this.Offset = toOffset; + return true; + } + + return false; + } + + private void NextColumn() + { + this.sourceColumn++; + if (this.TokenCandidate is not null) + { + this.TokenCandidate.NextColumn(); + } + } + + private void NextLine() + { + this.sourceLine++; + this.sourceColumn = 1; + if (this.TokenCandidate is not null) + { + this.TokenCandidate.NextLine(); + } + } + + private class TokenCandidateScope : IDisposable + { + private readonly ScanContext scanContext; + private bool disposed; + + public TokenCandidateScope(ScanContext scanContext) + { + this.scanContext = scanContext; + } + + public void Dispose() + { + if (!this.disposed) + { + this.scanContext.DiscardTokenCandidate(); + this.disposed = true; + } + } + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Scan/ScanResult.cs b/src/Rules.Framework.Rql/Pipeline/Scan/ScanResult.cs new file mode 100644 index 00000000..08fabff5 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Scan/ScanResult.cs @@ -0,0 +1,28 @@ +namespace Rules.Framework.Rql.Pipeline.Scan +{ + using System.Collections.Generic; + using Rules.Framework.Rql.Messages; + using Rules.Framework.Rql.Tokens; + + internal class ScanResult + { + private ScanResult(bool success, IReadOnlyList messages, IReadOnlyList tokens) + { + this.Success = success; + this.Messages = messages; + this.Tokens = tokens; + } + + public IReadOnlyList Messages { get; } + + public bool Success { get; } + + public IReadOnlyList Tokens { get; } + + public static ScanResult CreateError(IReadOnlyList messages) + => new ScanResult(success: false, messages, tokens: null); + + public static ScanResult CreateSuccess(IReadOnlyList tokens, IReadOnlyList messages) + => new ScanResult(success: true, messages, tokens); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs b/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs new file mode 100644 index 00000000..76b95f82 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs @@ -0,0 +1,392 @@ +namespace Rules.Framework.Rql.Pipeline.Scan +{ + using System; + using System.Collections.Generic; + using System.Globalization; + using System.Text.RegularExpressions; + using Rules.Framework.Rql.Messages; + using Rules.Framework.Rql.Tokens; + + internal class Scanner : IScanner + { + private const char DecimalSeparator = '.'; + + private static readonly Dictionary keywords = new Dictionary(StringComparer.Ordinal) + { + { nameof(TokenType.ACTIVATE), TokenType.ACTIVATE }, + { nameof(TokenType.ALL), TokenType.ALL }, + { nameof(TokenType.AND), TokenType.AND }, + { nameof(TokenType.APPLY), TokenType.APPLY }, + { nameof(TokenType.AS), TokenType.AS }, + { nameof(TokenType.ARRAY), TokenType.ARRAY }, + { nameof(TokenType.BOTTOM), TokenType.BOTTOM }, + { nameof(TokenType.CONTENT), TokenType.CONTENT }, + { nameof(TokenType.CREATE), TokenType.CREATE }, + { nameof(TokenType.DEACTIVATE), TokenType.DEACTIVATE }, + { nameof(TokenType.ELSE), TokenType.ELSE }, + { "FALSE", TokenType.BOOL }, + { nameof(TokenType.FOR), TokenType.FOR }, + { nameof(TokenType.FOREACH), TokenType.FOREACH }, + { nameof(TokenType.IF), TokenType.IF }, + { nameof(TokenType.IN), TokenType.IN }, + { nameof(TokenType.IS), TokenType.IS }, + { nameof(TokenType.MATCH), TokenType.MATCH }, + { nameof(TokenType.NAME), TokenType.NAME }, + { nameof(TokenType.NOT), TokenType.NOT }, + { nameof(TokenType.NOTHING), TokenType.NOTHING }, + { nameof(TokenType.NUMBER), TokenType.NUMBER }, + { nameof(TokenType.OBJECT), TokenType.OBJECT }, + { nameof(TokenType.ON), TokenType.ON }, + { nameof(TokenType.ONE), TokenType.ONE }, + { nameof(TokenType.OR), TokenType.OR }, + { nameof(TokenType.PRIORITY), TokenType.PRIORITY }, + { nameof(TokenType.RULE), TokenType.RULE }, + { nameof(TokenType.RULES), TokenType.RULES }, + { nameof(TokenType.SEARCH), TokenType.SEARCH }, + { nameof(TokenType.SET), TokenType.SET }, + { nameof(TokenType.SINCE), TokenType.SINCE }, + { nameof(TokenType.TO), TokenType.TO }, + { nameof(TokenType.TOP), TokenType.TOP }, + { "TRUE", TokenType.BOOL }, + { nameof(TokenType.UNTIL), TokenType.UNTIL }, + { nameof(TokenType.UPDATE), TokenType.UPDATE }, + { nameof(TokenType.VAR), TokenType.VAR }, + { nameof(TokenType.WHEN), TokenType.WHEN }, + { nameof(TokenType.WITH), TokenType.WITH }, + }; + + public Scanner() + { + } + + public ScanResult ScanTokens(string source) + { + if (source is null) + { + throw new ArgumentNullException(nameof(source)); + } + + var tokens = new List(); + using var messageContainer = new MessageContainer(); + if (!string.IsNullOrWhiteSpace(source)) + { + var scanContext = new ScanContext(source); + do + { + using (scanContext.BeginTokenCandidate()) + { + var token = ScanNextToken(scanContext); + if (token != Token.None) + { + tokens.Add(token); + } + + if (scanContext.TokenCandidate.HasError) + { + messageContainer.Error( + scanContext.TokenCandidate.Message, + scanContext.TokenCandidate.BeginPosition, + scanContext.TokenCandidate.EndPosition); + } + } + } while (scanContext.MoveNext()); + + using (scanContext.BeginTokenCandidate()) + { + CreateToken(scanContext, string.Empty, TokenType.EOF, literal: null!); + } + } + + var messages = messageContainer.Messages; + if (messageContainer.ErrorsCount > 0) + { + return ScanResult.CreateError(messages); + } + + return ScanResult.CreateSuccess(tokens, messages); + } + + private static void ConsumeAlphaNumeric(ScanContext scanContext) + { + while (IsAlphaNumeric(scanContext.GetNextChar()) && scanContext.MoveNext()) { } + } + + private static Token CreateToken(ScanContext scanContext, TokenType tokenType) + { + string lexeme = scanContext.ExtractLexeme(); + return CreateToken(scanContext, lexeme, tokenType, literal: null!); + } + + private static Token CreateToken(ScanContext scanContext, string lexeme, TokenType tokenType, object literal) + { + var isEscaped = lexeme.Length > 0 && IsEscape(lexeme[0]); + return Token.Create( + lexeme, + isEscaped, + literal, + scanContext.TokenCandidate.BeginPosition, + scanContext.TokenCandidate.EndPosition, + scanContext.TokenCandidate.Length, + tokenType); + } + + private static Token HandleDate(ScanContext scanContext) + { + string lexeme; + while (scanContext.GetNextChar() != '$' && scanContext.MoveNext()) + { + } + + if (scanContext.IsEof()) + { + lexeme = scanContext.ExtractLexeme(); + scanContext.TokenCandidate.MarkAsError($"Unterminated date '{lexeme}'."); + return Token.None; + } + + _ = scanContext.MoveNext(); + + // Trim the surrounding dollar symbols. + lexeme = scanContext.ExtractLexeme(); + var value = Regex.Unescape(lexeme.Substring(1, lexeme.Length - 2)); + if (!DateTime.TryParse(value, out _)) + { + scanContext.TokenCandidate.MarkAsError($"Invalid date '{lexeme}'."); + return Token.None; + } + + return CreateToken(scanContext, lexeme, TokenType.DATE, value); + } + + private static Token HandleIdentifier(ScanContext scanContext) + { + ConsumeAlphaNumeric(scanContext); + var lexeme = scanContext.ExtractLexeme(); + var lexemeUpper = lexeme.ToUpperInvariant(); + if (!keywords.TryGetValue(lexemeUpper, out TokenType type)) + { + return CreateToken(scanContext, lexeme, TokenType.IDENTIFIER, lexeme); + } + + if (type == TokenType.BOOL) + { + return CreateToken(scanContext, lexemeUpper, type, bool.Parse(lexeme)); + } + + return CreateToken(scanContext, type); + } + + private static Token HandleNumber(ScanContext scanContext) + { + string lexeme; + ConsumeDigits(scanContext); + + if (scanContext.GetNextChar() == DecimalSeparator && scanContext.MoveNext() && IsNumeric(scanContext.GetNextChar())) + { + ConsumeDigits(scanContext); + lexeme = scanContext.ExtractLexeme(); + return CreateToken(scanContext, lexeme, TokenType.DECIMAL, decimal.Parse(lexeme, CultureInfo.InvariantCulture)); + } + + if (ConsumeRemainingTokenCharacters(scanContext)) + { + lexeme = scanContext.ExtractLexeme(); + scanContext.TokenCandidate.MarkAsError($"Invalid number '{lexeme}'."); + return Token.None; + } + + lexeme = scanContext.ExtractLexeme(); + return CreateToken(scanContext, lexeme, TokenType.INT, int.Parse(lexeme, CultureInfo.InvariantCulture)); + + static void ConsumeDigits(ScanContext scanContext) + { + while (IsNumeric(scanContext.GetNextChar()) && scanContext.MoveNext()) { } + } + + static bool ConsumeRemainingTokenCharacters(ScanContext scanContext) + { + var consumed = false; + while ((IsAlphaNumeric(scanContext.GetNextChar()) || scanContext.GetNextChar() == DecimalSeparator) && scanContext.MoveNext()) + { + if (!consumed) + { + consumed = true; + } + } + + return consumed; + } + } + + private static Token HandlePlaceholder(ScanContext scanContext) + { + ConsumeAlphaNumeric(scanContext); + var lexeme = scanContext.ExtractLexeme(); + var literal = lexeme.Substring(1, lexeme.Length - 1); + return CreateToken(scanContext, lexeme, TokenType.PLACEHOLDER, literal); + } + + private static Token HandleString(ScanContext scanContext) + { + string lexeme; + while (scanContext.GetNextChar() != '"' && scanContext.MoveNext()) + { + // Support escaping double quotes. + if (scanContext.GetCurrentChar() == '\\' && scanContext.GetNextChar() == '"') + { + _ = scanContext.MoveNext(); + } + } + + if (scanContext.IsEof()) + { + lexeme = scanContext.ExtractLexeme(); + scanContext.TokenCandidate.MarkAsError($"Unterminated string '{lexeme}'."); + return Token.None; + } + + // The closing ". + _ = scanContext.MoveNext(); + + // Trim the surrounding quotes. + lexeme = scanContext.ExtractLexeme(); + var value = Regex.Unescape(lexeme.Substring(1, lexeme.Length - 2)); + return CreateToken(scanContext, lexeme, TokenType.STRING, value); + } + + private static bool IsAlpha(char @char) => @char >= 'A' && @char <= 'Z' || @char >= 'a' && @char <= 'z' || @char == '_'; + + private static bool IsAlphaNumeric(char @char) => IsAlpha(@char) || IsNumeric(@char); + + private static bool IsEscape(char @char) => @char == '#'; + + private static bool IsNumeric(char @char) => @char >= '0' && @char <= '9'; + + private static bool IsWhiteSpace(char @char) => @char == ' ' || @char == '\r' || @char == '\t' || @char == '\n'; + + private static Token ScanNextToken(ScanContext scanContext) + { + var @char = scanContext.GetCurrentChar(); + switch (@char) + { + case '(': + return CreateToken(scanContext, TokenType.BRACKET_LEFT); + + case ')': + return CreateToken(scanContext, TokenType.BRACKET_RIGHT); + + case '{': + return CreateToken(scanContext, TokenType.BRACE_LEFT); + + case '}': + return CreateToken(scanContext, TokenType.BRACE_RIGHT); + + case ';': + return CreateToken(scanContext, TokenType.SEMICOLON); + + case ',': + return CreateToken(scanContext, TokenType.COMMA); + + case '.': + return CreateToken(scanContext, TokenType.DOT); + + case '+': + return CreateToken(scanContext, TokenType.PLUS); + + case '-': + return CreateToken(scanContext, TokenType.MINUS); + + case '[': + return CreateToken(scanContext, TokenType.STRAIGHT_BRACKET_LEFT); + + case ']': + return CreateToken(scanContext, TokenType.STRAIGHT_BRACKET_RIGHT); + + case '/': + return CreateToken(scanContext, TokenType.SLASH); + + case '*': + return CreateToken(scanContext, TokenType.STAR); + + case '!': + if (scanContext.MoveNextConditionally('=')) + { + return CreateToken(scanContext, TokenType.NOT_EQUAL); + } + + scanContext.TokenCandidate.MarkAsError("Expected '=' after '!'"); + return Token.None; + + case '=': + if (scanContext.MoveNextConditionally('=')) + { + return CreateToken(scanContext, TokenType.EQUAL); + } + + return CreateToken(scanContext, TokenType.ASSIGN); + + case '>': + if (scanContext.MoveNextConditionally('=')) + { + return CreateToken(scanContext, TokenType.GREATER_THAN_OR_EQUAL); + } + + return CreateToken(scanContext, TokenType.GREATER_THAN); + + case '<': + if (scanContext.MoveNextConditionally('=')) + { + return CreateToken(scanContext, TokenType.LESS_THAN_OR_EQUAL); + } + + if (scanContext.MoveNextConditionally('>')) + { + return CreateToken(scanContext, TokenType.NOT_EQUAL); + } + + return CreateToken(scanContext, TokenType.LESS_THAN); + + case '$': + return HandleDate(scanContext); + + case ' ': + case '\r': + case '\t': + case '\n': + // Ignore whitespace. + return Token.None; + + case '@': + return HandlePlaceholder(scanContext); + + case '"': + return HandleString(scanContext); + + default: + if (IsNumeric(@char)) + { + return HandleNumber(scanContext); + } + + if (IsAlpha(@char)) + { + return HandleIdentifier(scanContext); + } + + if (IsEscape(@char)) + { + if (!scanContext.MoveNext()) + { + scanContext.TokenCandidate.MarkAsError($"Expected char after '{@char}'"); + return Token.None; + } + + return HandleIdentifier(scanContext); + } + + scanContext.TokenCandidate.MarkAsError($"Invalid char '{@char}'"); + return Token.None; + } + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Scan/TokenCandidateInfo.cs b/src/Rules.Framework.Rql/Pipeline/Scan/TokenCandidateInfo.cs new file mode 100644 index 00000000..8e435328 --- /dev/null +++ b/src/Rules.Framework.Rql/Pipeline/Scan/TokenCandidateInfo.cs @@ -0,0 +1,54 @@ +namespace Rules.Framework.Rql.Pipeline.Scan +{ + using System; + using Rules.Framework.Rql; + + internal class TokenCandidateInfo + { + public TokenCandidateInfo(uint startOffset, uint startLine, uint startColumn) + { + this.StartOffset = startOffset; + this.EndOffset = startOffset; + this.BeginPosition = RqlSourcePosition.From(startLine, startColumn); + this.EndPosition = RqlSourcePosition.From(startLine, startColumn); + this.HasError = false; + this.Message = null; + } + + public RqlSourcePosition BeginPosition { get; } + public uint EndOffset { get; private set; } + + public RqlSourcePosition EndPosition { get; private set; } + + public bool HasError { get; private set; } + + public uint Length => this.EndOffset + 1 - this.StartOffset; + + public string Message { get; private set; } + + public uint StartOffset { get; } + + public void MarkAsError(string message) + { + if (this.HasError) + { + throw new InvalidOperationException("An error has already been reported for specified token candidate."); + } + + this.HasError = true; + this.Message = message; + } + + public void NextColumn() + { + this.EndOffset++; + this.EndPosition = RqlSourcePosition.From(this.EndPosition.Line, this.EndPosition.Column + 1); + } + + public void NextLine() + { + this.EndOffset++; + this.EndPosition = RqlSourcePosition.From(this.EndPosition.Line + 1, 1); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/ReverseRqlBuilder.cs b/src/Rules.Framework.Rql/ReverseRqlBuilder.cs new file mode 100644 index 00000000..23c2c64f --- /dev/null +++ b/src/Rules.Framework.Rql/ReverseRqlBuilder.cs @@ -0,0 +1,234 @@ +namespace Rules.Framework.Rql +{ + using System; + using System.Linq; + using System.Text; + using Rules.Framework.Rql.Ast; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Ast.Segments; + using Rules.Framework.Rql.Ast.Statements; + + internal class ReverseRqlBuilder : IReverseRqlBuilder, IExpressionVisitor, ISegmentVisitor, IStatementVisitor + { + private const char SPACE = ' '; + + public string BuildRql(IAstElement astElement) + { + if (astElement is null) + { + throw new ArgumentNullException(nameof(astElement)); + } + + return astElement switch + { + Expression expression => expression.Accept(this), + Segment segment => segment.Accept(this), + Statement statement => statement.Accept(this), + _ => throw new NotSupportedException($"The given AST element is not supported: {astElement.GetType().FullName}."), + }; + } + + public string VisitAssignmentExpression(AssignmentExpression assignmentExpression) + { + var left = assignmentExpression.Left.Accept(this); + var right = assignmentExpression.Right.Accept(this); + return FormattableString.Invariant($"{left} {assignmentExpression.Assign.Lexeme} {right}"); + } + + public string VisitBinaryExpression(BinaryExpression binaryExpression) + => FormattableString.Invariant($"{binaryExpression.LeftExpression.Accept(this)} {binaryExpression.OperatorSegment.Accept(this)} {binaryExpression.RightExpression.Accept(this)}"); + + public string VisitCardinalitySegment(CardinalitySegment expression) + => $"{expression.CardinalityKeyword.Accept(this)} {expression.RuleKeyword.Accept(this)}"; + + public string VisitExpressionStatement(ExpressionStatement expressionStatement) + => $"{expressionStatement.Expression.Accept(this)};"; + + public string VisitIdentifierExpression(IdentifierExpression identifierExpression) => identifierExpression.Identifier.Lexeme; + + public string VisitInputConditionSegment(InputConditionSegment inputConditionExpression) + { + var left = inputConditionExpression.Left.Accept(this); + var @operator = inputConditionExpression.Operator.Lexeme; + var right = inputConditionExpression.Right.Accept(this); + return $"{left} {@operator} {right}"; + } + + public string VisitInputConditionsSegment(InputConditionsSegment inputConditionsExpression) + { + var inputConditionsRqlBuilder = new StringBuilder(); + if (inputConditionsExpression.InputConditions.Any()) + { + inputConditionsRqlBuilder.Append("WITH {"); + + var notFirst = false; + foreach (var inputConditionExpression in inputConditionsExpression.InputConditions) + { + if (notFirst) + { + inputConditionsRqlBuilder.Append(','); + } + else + { + notFirst = true; + } + + var inputCondition = inputConditionExpression.Accept(this); + inputConditionsRqlBuilder.Append(SPACE) + .Append(inputCondition); + } + + inputConditionsRqlBuilder.Append(SPACE) + .Append('}'); + } + + return inputConditionsRqlBuilder.ToString(); + } + + public string VisitKeywordExpression(KeywordExpression keywordExpression) => keywordExpression.Keyword.Lexeme.ToUpperInvariant(); + + public string VisitLiteralExpression(LiteralExpression literalExpression) => literalExpression.Type switch + { + LiteralType.String or LiteralType.Undefined => literalExpression.Token.Lexeme, + LiteralType.Bool => literalExpression.Value.ToString().ToUpperInvariant(), + LiteralType.Decimal or LiteralType.Integer => literalExpression.Value.ToString(), + LiteralType.DateTime => $"${literalExpression.Value:yyyy-MM-ddTHH:mm:ssZ}$", + _ => throw new NotSupportedException($"The literal type '{literalExpression.Type}' is not supported."), + }; + + public string VisitMatchExpression(MatchExpression matchExpression) + { + var cardinality = matchExpression.Cardinality.Accept(this); + var contentType = matchExpression.ContentType.Accept(this); + var matchDate = matchExpression.MatchDate.Accept(this); + var inputConditions = matchExpression.InputConditions.Accept(this); + + var matchRqlBuilder = new StringBuilder("MATCH") + .Append(SPACE) + .Append(cardinality) + .Append(SPACE) + .Append("FOR") + .Append(SPACE) + .Append(contentType) + .Append(SPACE) + .Append("ON") + .Append(SPACE) + .Append(matchDate); + + if (!string.IsNullOrWhiteSpace(inputConditions)) + { + matchRqlBuilder.Append(SPACE) + .Append(inputConditions); + } + + return matchRqlBuilder.ToString(); + } + + public string VisitNewArrayExpression(NewArrayExpression newArrayExpression) + { + var stringBuilder = new StringBuilder(newArrayExpression.Array.Lexeme) + .Append(SPACE) + .Append(newArrayExpression.InitializerBeginToken.Lexeme); + + if (newArrayExpression.Size != Expression.None) + { + stringBuilder.Append(newArrayExpression.Size.Accept(this)); + } + else + { + for (int i = 0; i < newArrayExpression.Values.Length; i++) + { + stringBuilder.Append(SPACE) + .Append(newArrayExpression.Values[i].Accept(this)); + + if (i < newArrayExpression.Values.Length - 1) + { + stringBuilder.Append(','); + } + } + + stringBuilder.Append(SPACE); + } + + return stringBuilder.Append(newArrayExpression.InitializerEndToken.Lexeme) + .ToString(); + } + + public string VisitNewObjectExpression(NewObjectExpression newObjectExpression) + { + var stringBuilder = new StringBuilder(newObjectExpression.Object.Lexeme); + + if (newObjectExpression.PropertyAssignments.Length > 0) + { + stringBuilder.AppendLine() + .Append('{'); + for (int i = 0; i < newObjectExpression.PropertyAssignments.Length; i++) + { + var propertyAssignment = newObjectExpression.PropertyAssignments[i].Accept(this); + stringBuilder.AppendLine() + .Append(new string(' ', 4)) + .Append(propertyAssignment); + + if (i < newObjectExpression.PropertyAssignments.Length - 1) + { + stringBuilder.Append(','); + } + } + stringBuilder.AppendLine() + .Append('}'); + } + + return stringBuilder.ToString(); + } + + public string VisitNoneExpression(NoneExpression noneExpression) => string.Empty; + + public string VisitNoneSegment(NoneSegment noneSegment) => string.Empty; + + public string VisitNoneStatement(NoneStatement noneStatement) => string.Empty; + + public string VisitOperatorSegment(OperatorSegment operatorExpression) + { +#if NETSTANDARD2_1_OR_GREATER + var separator = SPACE; +#else + var separator = new string(SPACE, 1); +#endif + return operatorExpression.Tokens.Select(t => t.Lexeme).Aggregate((t1, t2) => string.Join(separator, t1, t2)); + } + + public string VisitPlaceholderExpression(PlaceholderExpression placeholderExpression) => placeholderExpression.Token.Lexeme; + + public string VisitSearchExpression(SearchExpression searchExpression) + { + var contentType = searchExpression.ContentType.Accept(this); + var dateBegin = searchExpression.DateBegin.Accept(this); + var dateEnd = searchExpression.DateEnd.Accept(this); + var inputConditions = searchExpression.InputConditions.Accept(this); + + var searchRqlBuilder = new StringBuilder("SEARCH RULES") + .Append(SPACE) + .Append("FOR") + .Append(SPACE) + .Append(contentType) + .Append(SPACE) + .Append("SINCE") + .Append(SPACE) + .Append(dateBegin) + .Append(SPACE) + .Append("UNTIL") + .Append(SPACE) + .Append(dateEnd); + + if (!string.IsNullOrWhiteSpace(inputConditions)) + { + searchRqlBuilder.Append(SPACE) + .Append(inputConditions); + } + + return searchRqlBuilder.ToString(); + } + + public string VisitUnaryExpression(UnaryExpression unaryExpression) => $"{unaryExpression.Operator.Lexeme}{unaryExpression.Right.Accept(this)}"; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/RqlEngine.cs b/src/Rules.Framework.Rql/RqlEngine.cs new file mode 100644 index 00000000..17e46a3e --- /dev/null +++ b/src/Rules.Framework.Rql/RqlEngine.cs @@ -0,0 +1,129 @@ +namespace Rules.Framework.Rql +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Rules.Framework.Rql.Messages; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Pipeline.Parse; + using Rules.Framework.Rql.Pipeline.Scan; + using Rules.Framework.Rql.Runtime.Types; + + internal class RqlEngine : IRqlEngine + { + private const string ExceptionMessage = "Errors have occurred processing provided RQL source"; + private const string RqlErrorSourceUnavailable = ""; + private bool disposedValue; + private IInterpreter interpreter; + private IParser parser; + private IScanner scanner; + + public RqlEngine(RqlEngineArgs rqlEngineArgs) + { + this.scanner = rqlEngineArgs.Scanner; + this.parser = rqlEngineArgs.Parser; + this.interpreter = rqlEngineArgs.Interpreter; + } + + public void Dispose() + { + this.Dispose(disposing: true); + GC.SuppressFinalize(this); + } + + public async Task> ExecuteAsync(string rql) + { + var scanResult = this.scanner.ScanTokens(rql); + if (!scanResult.Success) + { + var errors = scanResult.Messages.Where(m => m.Severity == MessageSeverity.Error) + .Select(m => new RqlError(m.Text, RqlErrorSourceUnavailable, m.BeginPosition, m.EndPosition)) + .ToArray(); + throw new RqlException(ExceptionMessage, errors); + } + + var tokens = scanResult.Tokens; + var parserResult = parser.Parse(tokens); + if (!parserResult.Success) + { + var errors = parserResult.Messages.Where(m => m.Severity == MessageSeverity.Error) + .Select(m => new RqlError(m.Text, RqlErrorSourceUnavailable, m.BeginPosition, m.EndPosition)) + .ToArray(); + throw new RqlException(ExceptionMessage, errors); + } + + var statements = parserResult.Statements; + var interpretResult = await interpreter.InterpretAsync(statements).ConfigureAwait(false); + if (interpretResult.Success) + { + return interpretResult.Results.Select(s => ConvertResult(s)).ToArray(); + } + + var errorResults = interpretResult.Results.Where(s => s is ErrorStatementResult) + .Cast() + .Select(s => new RqlError(s.Message, s.Rql, s.BeginPosition, s.EndPosition)); + throw new RqlException(ExceptionMessage, errorResults); + } + + protected virtual void Dispose(bool disposing) + { + if (!disposedValue) + { + if (disposing) + { + this.interpreter = null!; + this.scanner = null!; + this.parser = null!; + } + + disposedValue = true; + } + } + + private static IResult ConvertResult(Pipeline.Interpret.IResult result) => result switch + { + NothingStatementResult nothingStatementResult => new NothingResult(nothingStatementResult.Rql), + ExpressionStatementResult expressionStatementResult when IsRulesSetResult(expressionStatementResult) => ConvertToRulesSetResult(expressionStatementResult), + ExpressionStatementResult expressionStatementResult => new ValueResult(expressionStatementResult.Rql, expressionStatementResult.Result), + _ => throw new NotSupportedException($"Result of type '{result.GetType().FullName}' is not supported."), + }; + + private static RulesSetResult ConvertToRulesSetResult(ExpressionStatementResult expressionStatementResult) + { + var rqlArray = (RqlArray)expressionStatementResult.Result; + var lines = new List>(rqlArray.Size); + for (int i = 0; i < rqlArray.Size; i++) + { + var rule = rqlArray.Value[i].Unwrap>(); + var rulesSetResultLine = new RulesSetResultLine(i + 1, rule); + lines.Add(rulesSetResultLine); + } + + return new RulesSetResult(expressionStatementResult.Rql, rqlArray.Size, lines); + } + + private static bool IsRulesSetResult(ExpressionStatementResult expressionStatementResult) + { + if (expressionStatementResult.Result is RqlArray rqlArray) + { + if (rqlArray.Size <= 0) + { + return false; + } + + for (int i = 0; i < rqlArray.Size; i++) + { + if (rqlArray.Value[i].UnderlyingType != RqlTypes.Rule) + { + return false; + } + } + + return true; + } + + return false; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/RqlEngineArgs.cs b/src/Rules.Framework.Rql/RqlEngineArgs.cs new file mode 100644 index 00000000..2788320b --- /dev/null +++ b/src/Rules.Framework.Rql/RqlEngineArgs.cs @@ -0,0 +1,19 @@ +namespace Rules.Framework.Rql +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Pipeline.Parse; + using Rules.Framework.Rql.Pipeline.Scan; + + [ExcludeFromCodeCoverage] + internal class RqlEngineArgs + { + public IInterpreter Interpreter { get; set; } + + public RqlOptions Options { get; set; } + + public IParser Parser { get; set; } + + public IScanner Scanner { get; set; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/RqlEngineBuilder.cs b/src/Rules.Framework.Rql/RqlEngineBuilder.cs new file mode 100644 index 00000000..1f1831f7 --- /dev/null +++ b/src/Rules.Framework.Rql/RqlEngineBuilder.cs @@ -0,0 +1,60 @@ +namespace Rules.Framework.Rql +{ + using System; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Pipeline.Parse; + using Rules.Framework.Rql.Pipeline.Scan; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Source; + + internal class RqlEngineBuilder + { + private readonly IRulesEngine rulesEngine; + private RqlOptions options; + + private RqlEngineBuilder(IRulesEngine rulesEngine) + { + this.rulesEngine = rulesEngine; + } + + public static RqlEngineBuilder CreateRqlEngine(IRulesEngine rulesEngine) + { + if (rulesEngine is null) + { + throw new ArgumentNullException(nameof(rulesEngine)); + } + + return new RqlEngineBuilder(rulesEngine); + } + + public IRqlEngine Build() + { + var runtime = RqlRuntime.Create(this.rulesEngine); + var scanner = new Scanner(); + var parseStrategyProvider = new ParseStrategyPool(); + var parser = new Parser(parseStrategyProvider); + var reverseRqlBuilder = new ReverseRqlBuilder(); + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + var args = new RqlEngineArgs + { + Interpreter = interpreter, + Options = this.options, + Parser = parser, + Scanner = scanner, + }; + + return new RqlEngine(args); + } + + public RqlEngineBuilder WithOptions(RqlOptions options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + this.options = options; + return this; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/RqlError.cs b/src/Rules.Framework.Rql/RqlError.cs new file mode 100644 index 00000000..22ee9625 --- /dev/null +++ b/src/Rules.Framework.Rql/RqlError.cs @@ -0,0 +1,26 @@ +namespace Rules.Framework.Rql +{ + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + public class RqlError + { + public RqlError(string text, string rql, RqlSourcePosition beginPosition, RqlSourcePosition endPosition) + { + this.Text = text; + this.Rql = rql; + this.BeginPosition = beginPosition; + this.EndPosition = endPosition; + } + + public RqlSourcePosition BeginPosition { get; } + + public RqlSourcePosition EndPosition { get; } + + public string Rql { get; } + + public string Text { get; } + + public override string ToString() => $"{this.Text} for source {this.Rql} @{this.BeginPosition}-{this.EndPosition}"; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/RqlException.cs b/src/Rules.Framework.Rql/RqlException.cs new file mode 100644 index 00000000..3f58292a --- /dev/null +++ b/src/Rules.Framework.Rql/RqlException.cs @@ -0,0 +1,50 @@ +namespace Rules.Framework.Rql +{ + using System; + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + using System.Linq; + using System.Text; + + [ExcludeFromCodeCoverage] + public class RqlException : Exception + { + public RqlException(string message, RqlError error) + : this(message, new[] { error }) + { + } + + public RqlException(string message, IEnumerable errors) + : base(ProcessMessage(message, errors)) + { + this.Errors = errors; + } + + public IEnumerable Errors { get; } + + public override string ToString() + { + var stringBuilder = new StringBuilder(base.ToString()); + stringBuilder.AppendLine() + .AppendLine("Errors:"); + foreach (var error in Errors) + { + stringBuilder.AppendFormat( + "---> {0} for RQL source '{1}' @ {2} to {3}", + error.Text, + error.Rql, + error.BeginPosition, + error.EndPosition); + } + + return stringBuilder.ToString(); + } + + private static string ProcessMessage(string message, IEnumerable errors) => errors.Count() switch + { + 0 => $"{message} - no error has been captured, please contact maintainers.", + 1 => $"{message} - {errors.First()}", + _ => $"{message} - multiple errors have occurred, check exception details.", + }; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/RqlOptions.cs b/src/Rules.Framework.Rql/RqlOptions.cs new file mode 100644 index 00000000..42573aaf --- /dev/null +++ b/src/Rules.Framework.Rql/RqlOptions.cs @@ -0,0 +1,18 @@ +namespace Rules.Framework.Rql +{ + using System; + using System.IO; + + public class RqlOptions + { + public TextWriter OutputWriter { get; set; } + + public static RqlOptions NewWithDefaults() + { + return new RqlOptions + { + OutputWriter = Console.Out, + }; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/RqlSourcePosition.cs b/src/Rules.Framework.Rql/RqlSourcePosition.cs new file mode 100644 index 00000000..27fcaa91 --- /dev/null +++ b/src/Rules.Framework.Rql/RqlSourcePosition.cs @@ -0,0 +1,24 @@ +namespace Rules.Framework.Rql +{ + using System.Runtime.InteropServices; + + [StructLayout(LayoutKind.Sequential)] + public readonly struct RqlSourcePosition + { + private RqlSourcePosition(uint line, uint column) + { + this.Line = line; + this.Column = column; + } + + public readonly uint Column; + + public readonly uint Line; + + public static RqlSourcePosition Empty { get; } = new RqlSourcePosition(0, 0); + + public static RqlSourcePosition From(uint line, uint column) => new RqlSourcePosition(line, column); + + public override string ToString() => $"{{{this.Line}:{this.Column}}}"; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/RuleEngineExtensions.cs b/src/Rules.Framework.Rql/RuleEngineExtensions.cs new file mode 100644 index 00000000..3bcb9d04 --- /dev/null +++ b/src/Rules.Framework.Rql/RuleEngineExtensions.cs @@ -0,0 +1,29 @@ +namespace Rules.Framework.Rql +{ + using System; + + public static class RuleEngineExtensions + { + public static IRqlEngine GetRqlEngine(this IRulesEngine rulesEngine) + { + return rulesEngine.GetRqlEngine(RqlOptions.NewWithDefaults()); + } + + public static IRqlEngine GetRqlEngine(this IRulesEngine rulesEngine, RqlOptions rqlOptions) + { + if (!typeof(TContentType).IsEnum) + { + throw new NotSupportedException($"Rule Query Language is not supported for non-enum types of {nameof(TContentType)}."); + } + + if (!typeof(TConditionType).IsEnum) + { + throw new NotSupportedException($"Rule Query Language is not supported for non-enum types of {nameof(TConditionType)}."); + } + + return RqlEngineBuilder.CreateRqlEngine(rulesEngine) + .WithOptions(rqlOptions) + .Build(); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Rules.Framework.Rql.csproj b/src/Rules.Framework.Rql/Rules.Framework.Rql.csproj new file mode 100644 index 00000000..83478e36 --- /dev/null +++ b/src/Rules.Framework.Rql/Rules.Framework.Rql.csproj @@ -0,0 +1,15 @@ + + + + netstandard2.0;netstandard2.1 + 10.0 + + + + + + + + + + diff --git a/src/Rules.Framework.Rql/RulesSetResult.cs b/src/Rules.Framework.Rql/RulesSetResult.cs new file mode 100644 index 00000000..d9dbdb3f --- /dev/null +++ b/src/Rules.Framework.Rql/RulesSetResult.cs @@ -0,0 +1,22 @@ +namespace Rules.Framework.Rql +{ + using System.Collections.Generic; + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + public class RulesSetResult : IResult + { + public RulesSetResult(string rql, int numberOfRules, IReadOnlyList> lines) + { + this.Rql = rql; + this.NumberOfRules = numberOfRules; + this.Lines = lines; + } + + public IReadOnlyList> Lines { get; } + + public int NumberOfRules { get; } + + public string Rql { get; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/RulesSetResultLine.cs b/src/Rules.Framework.Rql/RulesSetResultLine.cs new file mode 100644 index 00000000..101bab6a --- /dev/null +++ b/src/Rules.Framework.Rql/RulesSetResultLine.cs @@ -0,0 +1,19 @@ +namespace Rules.Framework.Rql +{ + using System.Diagnostics.CodeAnalysis; + using Rules.Framework.Rql.Runtime.Types; + + [ExcludeFromCodeCoverage] + public class RulesSetResultLine + { + internal RulesSetResultLine(int lineNumber, RqlRule rule) + { + this.LineNumber = lineNumber; + this.Rule = rule; + } + + public int LineNumber { get; } + + public RqlRule Rule { get; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/IPropertySet.cs b/src/Rules.Framework.Rql/Runtime/IPropertySet.cs new file mode 100644 index 00000000..a20e59d8 --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/IPropertySet.cs @@ -0,0 +1,9 @@ +using Rules.Framework.Rql.Runtime.Types; + +namespace Rules.Framework.Rql.Runtime +{ + internal interface IPropertySet + { + RqlAny SetPropertyValue(RqlString name, RqlAny value); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/IRuntime.cs b/src/Rules.Framework.Rql/Runtime/IRuntime.cs new file mode 100644 index 00000000..f7f32f7f --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/IRuntime.cs @@ -0,0 +1,18 @@ +namespace Rules.Framework.Rql.Runtime +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using Rules.Framework.Rql.Runtime.RuleManipulation; + using Rules.Framework.Rql.Runtime.Types; + + internal interface IRuntime + { + IRuntimeValue ApplyBinary(IRuntimeValue leftOperand, RqlOperators rqlOperator, IRuntimeValue rightOperand); + + IRuntimeValue ApplyUnary(IRuntimeValue value, RqlOperators rqlOperator); + + ValueTask MatchRulesAsync(MatchRulesArgs matchRulesArgs); + + ValueTask SearchRulesAsync(SearchRulesArgs searchRulesArgs); + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/IRuntimeValue.cs b/src/Rules.Framework.Rql/Runtime/IRuntimeValue.cs new file mode 100644 index 00000000..8a958a83 --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/IRuntimeValue.cs @@ -0,0 +1,14 @@ +namespace Rules.Framework.Rql.Runtime +{ + using System; + using Rules.Framework.Rql.Runtime.Types; + + internal interface IRuntimeValue + { + Type RuntimeType { get; } + + object RuntimeValue { get; } + + RqlType Type { get; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/MatchRulesArgs.cs b/src/Rules.Framework.Rql/Runtime/MatchRulesArgs.cs new file mode 100644 index 00000000..60871bcf --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/MatchRulesArgs.cs @@ -0,0 +1,17 @@ +namespace Rules.Framework.Rql.Runtime +{ + using System.Collections.Generic; + using Rules.Framework.Rql.Runtime.RuleManipulation; + using Rules.Framework.Rql.Runtime.Types; + + internal sealed class MatchRulesArgs + { + public IEnumerable> Conditions { get; set; } + + public TContentType ContentType { get; set; } + + public MatchCardinality MatchCardinality { get; set; } + + public RqlDate MatchDate { get; set; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/RqlOperators.cs b/src/Rules.Framework.Rql/Runtime/RqlOperators.cs new file mode 100644 index 00000000..965fc91c --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/RqlOperators.cs @@ -0,0 +1,23 @@ +namespace Rules.Framework.Rql.Runtime +{ + internal enum RqlOperators + { + None = 0, + Plus = 1, + Minus = 2, + Star = 3, + Slash = 4, + Mod = 5, + And = 6, + Or = 7, + Equals = 8, + NotEquals = 9, + GreaterThan = 10, + GreaterThanOrEquals = 11, + LesserThan = 12, + LesserThanOrEquals = 13, + In = 14, + NotIn = 15, + Assign = 16, + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/RqlRuntime.cs b/src/Rules.Framework.Rql/Runtime/RqlRuntime.cs new file mode 100644 index 00000000..93e01f31 --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/RqlRuntime.cs @@ -0,0 +1,167 @@ +namespace Rules.Framework.Rql.Runtime +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Threading.Tasks; + using Rules.Framework.Rql.Runtime.RuleManipulation; + using Rules.Framework.Rql.Runtime.Types; + + internal class RqlRuntime : IRuntime + { + private readonly IRulesEngine rulesEngine; + + private RqlRuntime(IRulesEngine rulesEngine) + { + this.rulesEngine = rulesEngine; + } + + public static IRuntime Create( + IRulesEngine rulesEngine) + { + return new RqlRuntime(rulesEngine); + } + + public IRuntimeValue ApplyBinary(IRuntimeValue leftOperand, RqlOperators rqlOperator, IRuntimeValue rightOperand) + { + leftOperand = EnsureUnwrapped(leftOperand); + rightOperand = EnsureUnwrapped(rightOperand); + switch (rqlOperator) + { + case RqlOperators.Slash: + return Divide(leftOperand, rightOperand); + + case RqlOperators.Minus: + return Subtract(leftOperand, rightOperand); + + case RqlOperators.Star: + return Multiply(leftOperand, rightOperand); + + case RqlOperators.Plus: + return Sum(leftOperand, rightOperand); + + default: + return new RqlNothing(); + } + } + + public IRuntimeValue ApplyUnary(IRuntimeValue value, RqlOperators rqlOperator) + { + value = EnsureUnwrapped(value); + if (rqlOperator == RqlOperators.Minus) + { + if (value is RqlInteger rqlInteger) + { + return new RqlInteger(-rqlInteger.Value); + } + + if (value is RqlDecimal rqlDecimal) + { + return new RqlDecimal(-rqlDecimal.Value); + } + } + + throw new RuntimeException($"Unary operator {rqlOperator} is not supported for value '{value}'."); + } + + public async ValueTask MatchRulesAsync(MatchRulesArgs matchRulesArgs) + { + if (matchRulesArgs.MatchCardinality == MatchCardinality.None) + { + throw new ArgumentException("A valid match cardinality must be provided.", nameof(matchRulesArgs)); + } + + if (matchRulesArgs.MatchCardinality == MatchCardinality.One) + { + var rule = await this.rulesEngine.MatchOneAsync(matchRulesArgs.ContentType, matchRulesArgs.MatchDate.Value, matchRulesArgs.Conditions).ConfigureAwait(false); + if (rule != null) + { + var rqlArrayOne = new RqlArray(1); + rqlArrayOne.SetAtIndex(0, new RqlRule(rule)); + return rqlArrayOne; + } + + return new RqlArray(0); + } + + var rules = await this.rulesEngine.MatchManyAsync(matchRulesArgs.ContentType, matchRulesArgs.MatchDate.Value, matchRulesArgs.Conditions).ConfigureAwait(false); + var rqlArrayAll = new RqlArray(rules.Count()); + var i = 0; + foreach (var rule in rules) + { + rqlArrayAll.SetAtIndex(i++, new RqlRule(rule)); + } + + return rqlArrayAll; + } + + public async ValueTask SearchRulesAsync(SearchRulesArgs searchRulesArgs) + { + var searchArgs = new SearchArgs( + searchRulesArgs.ContentType, + searchRulesArgs.DateBegin.Value, + searchRulesArgs.DateEnd.Value) + { + Conditions = searchRulesArgs.Conditions, + ExcludeRulesWithoutSearchConditions = true, + }; + + var rules = await this.rulesEngine.SearchAsync(searchArgs).ConfigureAwait(false); + var rqlArray = new RqlArray(rules.Count()); + var i = 0; + foreach (var rule in rules) + { + rqlArray.SetAtIndex(i++, new RqlRule(rule)); + } + + return rqlArray; + } + + private static IRuntimeValue Divide(IRuntimeValue leftOperand, IRuntimeValue rightOperand) => leftOperand switch + { + RqlInteger left when rightOperand is RqlInteger right => new RqlInteger(left.Value / right.Value), + RqlInteger when rightOperand is RqlDecimal => throw new RuntimeException($"Expected right operand of type {RqlTypes.Integer.Name} but found {RqlTypes.Decimal.Name}."), + RqlInteger => throw new RuntimeException($"Expected right operand of type {RqlTypes.Integer.Name} but found {rightOperand.Type.Name}."), + RqlDecimal left when rightOperand is RqlDecimal right => new RqlDecimal(left.Value / right.Value), + RqlDecimal when rightOperand is RqlInteger => throw new RuntimeException($"Expected right operand of type {RqlTypes.Decimal.Name} but found {RqlTypes.Integer.Name}."), + RqlDecimal => throw new RuntimeException($"Expected right operand of type {RqlTypes.Decimal.Name} but found {rightOperand.Type.Name}."), + _ => throw new RuntimeException($"Cannot divide operand of type {leftOperand.Type.Name}."), + }; + + private static IRuntimeValue EnsureUnwrapped(IRuntimeValue runtimeValue) + => runtimeValue.Type == RqlTypes.Any ? ((RqlAny)runtimeValue).Unwrap() : runtimeValue; + + private static IRuntimeValue Multiply(IRuntimeValue leftOperand, IRuntimeValue rightOperand) => leftOperand switch + { + RqlInteger left when rightOperand is RqlInteger right => new RqlInteger(left.Value * right.Value), + RqlInteger when rightOperand is RqlDecimal => throw new RuntimeException($"Expected right operand of type {RqlTypes.Integer.Name} but found {RqlTypes.Decimal.Name}."), + RqlInteger => throw new RuntimeException($"Expected right operand of type {RqlTypes.Integer.Name} but found {rightOperand.Type.Name}."), + RqlDecimal left when rightOperand is RqlDecimal right => new RqlDecimal(left.Value * right.Value), + RqlDecimal when rightOperand is RqlInteger => throw new RuntimeException($"Expected right operand of type {RqlTypes.Decimal.Name} but found {RqlTypes.Integer.Name}."), + RqlDecimal => throw new RuntimeException($"Expected right operand of type {RqlTypes.Decimal.Name} but found {rightOperand.Type.Name}."), + _ => throw new RuntimeException($"Cannot multiply operand of type {leftOperand.Type.Name}."), + }; + + private static IRuntimeValue Subtract(IRuntimeValue leftOperand, IRuntimeValue rightOperand) => leftOperand switch + { + RqlInteger left when rightOperand is RqlInteger right => new RqlInteger(left.Value - right.Value), + RqlInteger when rightOperand is RqlDecimal => throw new RuntimeException($"Expected right operand of type {RqlTypes.Integer.Name} but found {RqlTypes.Decimal.Name}."), + RqlInteger => throw new RuntimeException($"Expected right operand of type {RqlTypes.Integer.Name} but found {rightOperand.Type.Name}."), + RqlDecimal left when rightOperand is RqlDecimal right => new RqlDecimal(left.Value - right.Value), + RqlDecimal when rightOperand is RqlInteger => throw new RuntimeException($"Expected right operand of type {RqlTypes.Decimal.Name} but found {RqlTypes.Integer.Name}."), + RqlDecimal => throw new RuntimeException($"Expected right operand of type {RqlTypes.Decimal.Name} but found {rightOperand.Type.Name}."), + _ => throw new RuntimeException($"Cannot subtract operand of type {leftOperand.Type.Name}."), + }; + + private static IRuntimeValue Sum(IRuntimeValue leftOperand, IRuntimeValue rightOperand) => leftOperand switch + { + RqlInteger left when rightOperand is RqlInteger right => new RqlInteger(left.Value + right.Value), + RqlInteger when rightOperand is RqlDecimal => throw new RuntimeException($"Expected right operand of type {RqlTypes.Integer.Name} but found {RqlTypes.Decimal.Name}."), + RqlInteger => throw new RuntimeException($"Expected right operand of type {RqlTypes.Integer.Name} but found {rightOperand.Type.Name}."), + RqlDecimal left when rightOperand is RqlDecimal right => new RqlDecimal(left.Value + right.Value), + RqlDecimal when rightOperand is RqlInteger => throw new RuntimeException($"Expected right operand of type {RqlTypes.Decimal.Name} but found {RqlTypes.Integer.Name}."), + RqlDecimal => throw new RuntimeException($"Expected right operand of type {RqlTypes.Decimal.Name} but found {rightOperand.Type.Name}."), + _ => throw new RuntimeException($"Cannot sum operand of type {leftOperand.Type.Name}."), + }; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/RuleManipulation/MatchCardinality.cs b/src/Rules.Framework.Rql/Runtime/RuleManipulation/MatchCardinality.cs new file mode 100644 index 00000000..d7b7cdf9 --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/RuleManipulation/MatchCardinality.cs @@ -0,0 +1,9 @@ +namespace Rules.Framework.Rql.Runtime.RuleManipulation +{ + internal enum MatchCardinality + { + None = 0, + One = 1, + All = 2, + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/RuntimeException.cs b/src/Rules.Framework.Rql/Runtime/RuntimeException.cs new file mode 100644 index 00000000..7049c0c9 --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/RuntimeException.cs @@ -0,0 +1,23 @@ +namespace Rules.Framework.Rql.Runtime +{ + using System; + using System.Collections.Generic; + using System.Linq; + + internal class RuntimeException : Exception + { + public RuntimeException(string error) + : base(error) + { + this.Errors = new[] { error }; + } + + public RuntimeException(IEnumerable errors) + : base(errors.Aggregate((e1, e2) => $"{e1}{Environment.NewLine}{e2}")) + { + this.Errors = errors; + } + + public IEnumerable Errors { get; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/SearchRulesArgs.cs b/src/Rules.Framework.Rql/Runtime/SearchRulesArgs.cs new file mode 100644 index 00000000..fd4eb2b6 --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/SearchRulesArgs.cs @@ -0,0 +1,16 @@ +namespace Rules.Framework.Rql.Runtime +{ + using System.Collections.Generic; + using Rules.Framework.Rql.Runtime.Types; + + internal sealed class SearchRulesArgs + { + public IEnumerable> Conditions { get; set; } + + public TContentType ContentType { get; set; } + + public RqlDate DateBegin { get; set; } + + public RqlDate DateEnd { get; set; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlAny.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlAny.cs new file mode 100644 index 00000000..074c53f5 --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlAny.cs @@ -0,0 +1,45 @@ +namespace Rules.Framework.Rql.Runtime.Types +{ + using System; + using Rules.Framework.Rql.Runtime; + + public readonly struct RqlAny : IRuntimeValue + { + private static readonly RqlType type = RqlTypes.Any; + + private readonly IRuntimeValue underlyingRuntimeValue; + + public RqlAny() + : this(new RqlNothing()) + { + } + + internal RqlAny(IRuntimeValue value) + { + var underlyingRuntimeValue = value; + while (underlyingRuntimeValue is RqlAny rqlAny) + { + underlyingRuntimeValue = rqlAny.Unwrap(); + } + + this.underlyingRuntimeValue = underlyingRuntimeValue; + } + + public Type RuntimeType => this.underlyingRuntimeValue.RuntimeType; + + public object RuntimeValue => this.underlyingRuntimeValue.RuntimeValue; + + public RqlType Type => type; + + public RqlType UnderlyingType => this.underlyingRuntimeValue.Type; + + public object Value => this.underlyingRuntimeValue.RuntimeValue; + + public override string ToString() + => $"<{this.Type.Name}> ({this.underlyingRuntimeValue.ToString()})"; + + internal IRuntimeValue Unwrap() => this.underlyingRuntimeValue; + + internal T Unwrap() where T : IRuntimeValue => (T)this.underlyingRuntimeValue; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlArray.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlArray.cs new file mode 100644 index 00000000..fcf2ff8f --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlArray.cs @@ -0,0 +1,119 @@ +namespace Rules.Framework.Rql.Runtime.Types +{ + using System; + using System.Collections.Generic; + using System.Text; + using Rules.Framework.Rql.Runtime; + + public readonly struct RqlArray : IRuntimeValue + { + private static readonly Type runtimeType = typeof(object[]); + private static readonly RqlType type = RqlTypes.Array; + private readonly int size; + + public RqlArray(int size) + : this(size, true) + { + } + + internal RqlArray(int size, bool shouldInitializeElements) + { + this.size = size; + this.Value = new RqlAny[size]; + if (shouldInitializeElements) + { +#if NETSTANDARD2_1_OR_GREATER + Array.Fill(this.Value, new RqlAny()); +#else + for (var i = 0; i < this.size; i++) + { + this.Value[i] = new RqlAny(); + } +#endif + } + } + + public Type RuntimeType => runtimeType; + + public object RuntimeValue => ConvertToNativeArray(this); + + public RqlInteger Size => this.size; + + public RqlType Type => type; + + public readonly RqlAny[] Value { get; } + + public static object[] ConvertToNativeArray(RqlArray rqlArray) + { + var result = new object[rqlArray.size]; + for (int i = 0; i < rqlArray.size; i++) + { + result[i] = rqlArray.Value[i].RuntimeValue; + } + + return result; + } + + public static implicit operator RqlAny(RqlArray rqlArray) => new RqlAny(rqlArray); + + public RqlNothing SetAtIndex(RqlInteger index, RqlAny value) + { + if (index.Value < 0 || index.Value >= this.size) + { + throw new ArgumentOutOfRangeException(nameof(index), index, $"The value of '{index}' is out of the '{nameof(RqlArray)}' range."); + } + + this.Value[index.Value] = value; + return new RqlNothing(); + } + + public override string ToString() + => this.ToString(0); + + internal string ToString(int indent) + { + var stringBuilder = new StringBuilder() + .Append('<') + .Append(this.Type.Name) + .Append('>') + .Append(' '); + + if (this.size > 0) + { + stringBuilder.AppendLine() + .Append(new string(' ', indent)) + .Append('{') + .AppendLine(); + var min = Math.Min(this.size, 5); + for (int i = 0; i < min; i++) + { + stringBuilder.Append(new string(' ', indent + 4)) + .Append(this.Value[i]); + if (i < min - 1) + { + stringBuilder.Append(',') + .AppendLine(); + } + } + + if (min < this.size) + { + stringBuilder.Append(',') + .AppendLine() + .Append(new string(' ', indent + 4)) + .Append("..."); + } + + stringBuilder.AppendLine() + .Append(new string(' ', indent)) + .Append('}'); + } + else + { + stringBuilder.Append("{ (empty) }"); + } + + return stringBuilder.ToString(); + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlBool.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlBool.cs new file mode 100644 index 00000000..341aa30e --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlBool.cs @@ -0,0 +1,33 @@ +namespace Rules.Framework.Rql.Runtime.Types +{ + using System; + using Rules.Framework.Rql.Runtime; + + public readonly struct RqlBool : IRuntimeValue + { + private static readonly Type runtimeType = typeof(bool); + private static readonly RqlType type = RqlTypes.Bool; + + internal RqlBool(bool value) + { + this.Value = value; + } + + public Type RuntimeType => runtimeType; + + public object RuntimeValue => this.Value; + + public RqlType Type => type; + + public readonly bool Value { get; } + + public static implicit operator bool(RqlBool rqlBool) => rqlBool.Value; + + public static implicit operator RqlAny(RqlBool rqlBool) => new RqlAny(rqlBool); + + public static implicit operator RqlBool(bool value) => new RqlBool(value); + + public override string ToString() + => $"<{Type.Name}> {this.Value}"; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlDate.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlDate.cs new file mode 100644 index 00000000..5fee8994 --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlDate.cs @@ -0,0 +1,33 @@ +namespace Rules.Framework.Rql.Runtime.Types +{ + using System; + using Rules.Framework.Rql.Runtime; + + public readonly struct RqlDate : IRuntimeValue + { + private static readonly Type runtimeType = typeof(DateTime); + private static readonly RqlType type = RqlTypes.Date; + + internal RqlDate(DateTime value) + { + this.Value = value; + } + + public Type RuntimeType => runtimeType; + + public object RuntimeValue => this.Value; + + public RqlType Type => type; + + public readonly DateTime Value { get; } + + public static implicit operator DateTime(RqlDate rqlDate) => rqlDate.Value; + + public static implicit operator RqlAny(RqlDate rqlDate) => new RqlAny(rqlDate); + + public static implicit operator RqlDate(DateTime value) => new RqlDate(value); + + public override string ToString() + => $"<{Type.Name}> {this.Value:g}"; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlDecimal.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlDecimal.cs new file mode 100644 index 00000000..49a7eb4b --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlDecimal.cs @@ -0,0 +1,33 @@ +namespace Rules.Framework.Rql.Runtime.Types +{ + using System; + using Rules.Framework.Rql.Runtime; + + public readonly struct RqlDecimal : IRuntimeValue + { + private static readonly Type runtimeType = typeof(decimal); + private static readonly RqlType type = RqlTypes.Decimal; + + internal RqlDecimal(decimal value) + { + this.Value = value; + } + + public Type RuntimeType => runtimeType; + + public object RuntimeValue => this.Value; + + public RqlType Type => type; + + public readonly decimal Value { get; } + + public static implicit operator decimal(RqlDecimal rqlDecimal) => rqlDecimal.Value; + + public static implicit operator RqlAny(RqlDecimal rqlDecimal) => new RqlAny(rqlDecimal); + + public static implicit operator RqlDecimal(decimal value) => new RqlDecimal(value); + + public override string ToString() + => $"<{Type.Name}> {this.Value}"; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlInteger.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlInteger.cs new file mode 100644 index 00000000..18f1707b --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlInteger.cs @@ -0,0 +1,33 @@ +namespace Rules.Framework.Rql.Runtime.Types +{ + using System; + using Rules.Framework.Rql.Runtime; + + public readonly struct RqlInteger : IRuntimeValue + { + private static readonly Type runtimeType = typeof(int); + private static readonly RqlType type = RqlTypes.Integer; + + internal RqlInteger(int value) + { + this.Value = value; + } + + public Type RuntimeType => runtimeType; + + public object RuntimeValue => this.Value; + + public RqlType Type => type; + + public readonly int Value { get; } + + public static implicit operator int(RqlInteger rqlInteger) => rqlInteger.Value; + + public static implicit operator RqlAny(RqlInteger rqlInteger) => new RqlAny(rqlInteger); + + public static implicit operator RqlInteger(int value) => new RqlInteger(value); + + public override string ToString() + => $"<{Type.Name}> {this.Value}"; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlNothing.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlNothing.cs new file mode 100644 index 00000000..809779cd --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlNothing.cs @@ -0,0 +1,21 @@ +namespace Rules.Framework.Rql.Runtime.Types +{ + using System; + using Rules.Framework.Rql.Runtime; + + public readonly struct RqlNothing : IRuntimeValue + { + private static readonly Type runtimeType = typeof(object); + private static readonly RqlType type = RqlTypes.Nothing; + public Type RuntimeType => runtimeType; + + public object RuntimeValue => null; + + public RqlType Type => type; + + public static implicit operator RqlAny(RqlNothing rqlNothing) => new RqlAny(rqlNothing); + + public override string ToString() + => $"<{Type.Name}>"; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlObject.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlObject.cs new file mode 100644 index 00000000..76961f0a --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlObject.cs @@ -0,0 +1,84 @@ +namespace Rules.Framework.Rql.Runtime.Types +{ + using System; + using System.Collections.Generic; + using System.Text; + using Rules.Framework.Rql.Runtime; + + public readonly struct RqlObject : IRuntimeValue, IPropertySet + { + private static readonly Type runtimeType = typeof(object); + private static readonly RqlType type = RqlTypes.Object; + private readonly Dictionary properties; + + public RqlObject() + { + this.properties = new Dictionary(StringComparer.Ordinal); + } + + public Type RuntimeType => runtimeType; + + public object RuntimeValue => this.Value; + + public RqlType Type => type; + + public object Value => ConvertToDictionary(this); + + public static implicit operator RqlAny(RqlObject rqlObject) => new RqlAny(rqlObject); + + public RqlAny SetPropertyValue(RqlString name, RqlAny value) => this.properties[name.Value] = value; + + public override string ToString() + => $"<{Type.Name}>{Environment.NewLine}{this.ToString(4)}"; + + internal string ToString(int indent) + { + var stringBuilder = new StringBuilder() + .Append('{'); + + foreach (var property in this.properties) + { + stringBuilder.AppendLine() + .Append(new string(' ', indent)) + .Append(property.Key) + .Append(": "); + + if (property.Value.UnderlyingType == RqlTypes.Object) + { + stringBuilder.Append(property.Value.Unwrap().ToString(indent + 4)); + continue; + } + + if (property.Value.UnderlyingType == RqlTypes.ReadOnlyObject) + { + stringBuilder.Append(property.Value.Unwrap().ToString(indent + 4)); + continue; + } + + if (property.Value.UnderlyingType == RqlTypes.Array) + { + stringBuilder.Append(property.Value.Unwrap().ToString(indent)); + continue; + } + + stringBuilder.Append(property.Value.Value); + } + + return stringBuilder.AppendLine() + .Append(new string(' ', indent - 4)) + .Append('}') + .ToString(); + } + + private static IDictionary ConvertToDictionary(RqlObject value) + { + var result = new Dictionary(StringComparer.Ordinal); + foreach (var kvp in value.properties) + { + result[kvp.Key] = kvp.Value.RuntimeValue; + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlReadOnlyObject.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlReadOnlyObject.cs new file mode 100644 index 00000000..c215802c --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlReadOnlyObject.cs @@ -0,0 +1,81 @@ +namespace Rules.Framework.Rql.Runtime.Types +{ + using System; + using System.Collections.Generic; + using System.Text; + + public readonly struct RqlReadOnlyObject : IRuntimeValue + { + private static readonly Type runtimeType = typeof(object); + private static readonly RqlType type = RqlTypes.ReadOnlyObject; + private readonly IDictionary properties; + + internal RqlReadOnlyObject(IDictionary properties) + { + this.properties = properties; + } + + public Type RuntimeType => runtimeType; + + public object RuntimeValue => this.Value; + + public RqlType Type => type; + + public object Value => ConvertToDictionary(this); + + public static implicit operator RqlAny(RqlReadOnlyObject rqlReadOnlyObject) => new RqlAny(rqlReadOnlyObject); + + public override string ToString() + => $"<{Type.Name}>{Environment.NewLine}{this.ToString(4)}"; + + internal string ToString(int indent) + { + var stringBuilder = new StringBuilder() + .Append('{'); + + foreach (var property in this.properties) + { + stringBuilder.AppendLine() + .Append(new string(' ', indent)) + .Append(property.Key) + .Append(": "); + + if (property.Value.UnderlyingType == RqlTypes.Object) + { + stringBuilder.Append(property.Value.Unwrap().ToString(indent + 4)); + continue; + } + + if (property.Value.UnderlyingType == RqlTypes.ReadOnlyObject) + { + stringBuilder.Append(property.Value.Unwrap().ToString(indent + 4)); + continue; + } + + if (property.Value.UnderlyingType == RqlTypes.Array) + { + stringBuilder.Append(property.Value.Unwrap().ToString()); + continue; + } + + stringBuilder.Append(property.Value.Value); + } + + return stringBuilder.AppendLine() + .Append(new string(' ', indent - 4)) + .Append('}') + .ToString(); + } + + private static IDictionary ConvertToDictionary(RqlReadOnlyObject value) + { + var result = new Dictionary(StringComparer.Ordinal); + foreach (var kvp in value.properties) + { + result[kvp.Key] = kvp.Value.RuntimeValue; + } + + return result; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlRule.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlRule.cs new file mode 100644 index 00000000..e77c75d2 --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlRule.cs @@ -0,0 +1,146 @@ +namespace Rules.Framework.Rql.Runtime.Types +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using Rules.Framework.Core; + using Rules.Framework.Core.ConditionNodes; + + public readonly struct RqlRule : IRuntimeValue + { + private static readonly Type runtimeType = typeof(Rule); + private static readonly RqlType type = RqlTypes.Rule; + private readonly Dictionary properties; + + internal RqlRule(Rule rule) + { + this.Value = rule; + this.properties = new Dictionary(StringComparer.Ordinal) + { + { "Active", new RqlBool(rule.Active) }, + { "DateBegin", new RqlDate(rule.DateBegin) }, + { "DateEnd", rule.DateEnd.HasValue ? new RqlDate(rule.DateEnd.Value) : new RqlNothing() }, + { "Name", new RqlString(rule.Name) }, + { "Priority", new RqlInteger(rule.Priority) }, + { "RootCondition", rule.RootCondition is not null ? ConvertCondition(rule.RootCondition) : new RqlNothing() }, + }; + } + + public Type RuntimeType => runtimeType; + + public object RuntimeValue => this.Value; + + public RqlType Type => type; + + public readonly Rule Value { get; } + + public static implicit operator RqlAny(RqlRule rqlRule) => new RqlAny(rqlRule); + + public override string ToString() + => $"<{Type.Name}>{Environment.NewLine}{this.ToString(4)}"; + + internal string ToString(int indent) + { + var stringBuilder = new StringBuilder() + .Append('{'); + + foreach (var property in this.properties) + { + stringBuilder.AppendLine() + .Append(new string(' ', indent)) + .Append(property.Key) + .Append(": "); + + if (property.Value.UnderlyingType == RqlTypes.Object) + { + stringBuilder.Append(property.Value.Unwrap().ToString(indent + 4)); + continue; + } + + if (property.Value.UnderlyingType == RqlTypes.ReadOnlyObject) + { + stringBuilder.Append(property.Value.Unwrap().ToString(indent + 4)); + continue; + } + + if (property.Value.UnderlyingType == RqlTypes.Array) + { + stringBuilder.Append(property.Value.Unwrap().ToString()); + continue; + } + + stringBuilder.Append(property.Value.Value); + } + + return stringBuilder.AppendLine() + .Append(new string(' ', indent - 4)) + .Append('}') + .ToString(); + } + + private static RqlAny ConvertCondition(IConditionNode condition) + { + switch (condition) + { + case ComposedConditionNode ccn: + var childConditions = new RqlArray(ccn.ChildConditionNodes.Count()); + var i = 0; + foreach (var childConditionNode in ccn.ChildConditionNodes) + { + childConditions.SetAtIndex(i++, ConvertCondition(childConditionNode)); + } + + var composedConditionProperties = new Dictionary(StringComparer.Ordinal) + { + { "ChildConditionNodes", childConditions }, + { "LogicalOperator", new RqlString(ccn.LogicalOperator.ToString()) }, + }; + return new RqlReadOnlyObject(composedConditionProperties); + + case ValueConditionNode vcn: + var valueConditionProperties = new Dictionary(StringComparer.Ordinal) + { + { "ConditionType", new RqlString(vcn.ConditionType.ToString()) }, + { "DataType", new RqlString(vcn.DataType.ToString()) }, + { "LogicalOperator", new RqlString(vcn.LogicalOperator.ToString()) }, + { "Operand", ConvertValue(vcn.Operand) }, + { "Operator", new RqlString(vcn.Operator.ToString()) }, + }; + return new RqlReadOnlyObject(valueConditionProperties); + + default: + throw new NotSupportedException($"Specified condition node type is not supported: {condition.GetType().FullName}"); + } + } + + private static RqlAny ConvertValue(object value) + { + return value switch + { + IEnumerable intArray => CreateArray(intArray), + IEnumerable decimalArray => CreateArray(decimalArray), + IEnumerable boolArray => CreateArray(boolArray), + IEnumerable stringArray => CreateArray(stringArray), + int i => new RqlInteger(i), + decimal d => new RqlDecimal(d), + bool b => new RqlBool(b), + string s => new RqlString(s), + null => new RqlNothing(), + _ => throw new NotSupportedException($"Specified value is not supported for conversion to RQL type system: {value.GetType().FullName}"), + }; + } + + private static RqlArray CreateArray(IEnumerable source) + { + var count = source.Count(); + var rqlArray = new RqlArray(count); + for (var i = 0; i < count; i++) + { + rqlArray.SetAtIndex(i, ConvertValue(source.ElementAt(i)!)); + } + + return rqlArray; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlString.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlString.cs new file mode 100644 index 00000000..17cff583 --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlString.cs @@ -0,0 +1,34 @@ +namespace Rules.Framework.Rql.Runtime.Types +{ + using System; + using System.Collections.Generic; + using Rules.Framework.Rql.Runtime; + + public readonly struct RqlString : IRuntimeValue + { + private static readonly Type runtimeType = typeof(string); + private static readonly RqlType type = RqlTypes.String; + + internal RqlString(string value) + { + this.Value = value ?? string.Empty; + } + + public Type RuntimeType => runtimeType; + + public object RuntimeValue => this.Value; + + public RqlType Type => type; + + public readonly string Value { get; } + + public static implicit operator RqlAny(RqlString rqlString) => new RqlAny(rqlString); + + public static implicit operator RqlString(string value) => new RqlString(value); + + public static implicit operator string(RqlString rqlString) => rqlString.Value; + + public override string ToString() + => @$"<{Type.Name}> ""{this.Value}"""; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlType.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlType.cs new file mode 100644 index 00000000..542250b5 --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlType.cs @@ -0,0 +1,55 @@ +namespace Rules.Framework.Rql.Runtime.Types +{ + using System; + using System.Collections.Generic; + + public readonly struct RqlType + { + private readonly IDictionary assignableTypes; + + public RqlType(string name) + { + if (string.IsNullOrWhiteSpace(name)) + { + throw new ArgumentException($"'{nameof(name)}' cannot be null or whitespace.", nameof(name)); + } + + this.Name = name; + this.assignableTypes = new Dictionary(StringComparer.Ordinal); + } + + public IEnumerable AssignableTypes => this.assignableTypes.Values; + + public string Name { get; } + + public static bool operator !=(RqlType left, RqlType right) => !(left == right); + + public static bool operator ==(RqlType left, RqlType right) => string.Equals(left.Name, right.Name, StringComparison.Ordinal); + + public bool IsAssignableTo(RqlType rqlType) + { + if (string.Equals(rqlType.Name, this.Name, StringComparison.Ordinal)) + { + return true; + } + + return this.assignableTypes.ContainsKey(rqlType.Name); + } + + internal void AddAssignableType(RqlType rqlType) + { + string rqlTypeName = rqlType.Name; + if (string.Equals(rqlTypeName, this.Name, StringComparison.Ordinal)) + { + throw new InvalidOperationException("Type already is assignable to itself."); + } + + if (this.assignableTypes.ContainsKey(rqlTypeName)) + { + throw new InvalidOperationException($"Assignable type '{rqlType.Name}' has already been added to {this.Name}."); + } + + this.assignableTypes[rqlTypeName] = rqlType; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlTypes.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlTypes.cs new file mode 100644 index 00000000..39cb43a1 --- /dev/null +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlTypes.cs @@ -0,0 +1,55 @@ +namespace Rules.Framework.Rql.Runtime.Types +{ + public static class RqlTypes + { + static RqlTypes() + { + // Types bootstrap. + Any = new RqlType("any"); + Array = new RqlType("array"); + Bool = new RqlType("bool"); + Date = new RqlType("date"); + Decimal = new RqlType("decimal"); + Integer = new RqlType("integer"); + Nothing = new RqlType("nothing"); + Object = new RqlType("object"); + ReadOnlyObject = new RqlType("read_only_object"); + Rule = new RqlType("rule"); + String = new RqlType("string"); + + // Register assignables. + Array.AddAssignableType(Any); + Bool.AddAssignableType(Any); + Date.AddAssignableType(Any); + Decimal.AddAssignableType(Any); + Integer.AddAssignableType(Any); + Nothing.AddAssignableType(Any); + Object.AddAssignableType(Any); + ReadOnlyObject.AddAssignableType(Any); + Rule.AddAssignableType(Any); + String.AddAssignableType(Any); + } + + public static RqlType Any { get; } + + public static RqlType Array { get; } + + public static RqlType Bool { get; } + + public static RqlType Date { get; } + + public static RqlType Decimal { get; } + + public static RqlType Integer { get; } + + public static RqlType Nothing { get; } + + public static RqlType Object { get; } + + public static RqlType ReadOnlyObject { get; } + + public static RqlType Rule { get; } + + public static RqlType String { get; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Tokens/AllowAsIdentifierAttribute.cs b/src/Rules.Framework.Rql/Tokens/AllowAsIdentifierAttribute.cs new file mode 100644 index 00000000..5ae30b13 --- /dev/null +++ b/src/Rules.Framework.Rql/Tokens/AllowAsIdentifierAttribute.cs @@ -0,0 +1,12 @@ +namespace Rules.Framework.Rql.Tokens +{ + using System; + using System.Diagnostics.CodeAnalysis; + + [AttributeUsage(AttributeTargets.Field)] + [ExcludeFromCodeCoverage] + internal class AllowAsIdentifierAttribute : Attribute + { + public bool RequireEscaping { get; set; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Tokens/Constants.cs b/src/Rules.Framework.Rql/Tokens/Constants.cs new file mode 100644 index 00000000..aef204b0 --- /dev/null +++ b/src/Rules.Framework.Rql/Tokens/Constants.cs @@ -0,0 +1,29 @@ +namespace Rules.Framework.Rql.Tokens +{ + using System; + using System.Linq; + using System.Reflection; + + internal static class Constants + { + private static readonly TokenType[] allowedEscapedIdentifierNames; + + private static readonly TokenType[] allowedUnescapedIdentifierNames; + + static Constants() + { + var tokenTypeType = typeof(TokenType); + var allowedEscapedIdentifierMembers = tokenTypeType.GetMembers() + .Where(mi => mi.GetCustomAttribute() is not null); + allowedEscapedIdentifierNames = allowedEscapedIdentifierMembers.Select(mi => (TokenType)Enum.Parse(typeof(TokenType), mi.Name)) + .ToArray(); + allowedUnescapedIdentifierNames = allowedEscapedIdentifierMembers.Where(mi => !mi.GetCustomAttribute().RequireEscaping) + .Select(mi => (TokenType)Enum.Parse(typeof(TokenType), mi.Name)) + .ToArray(); + } + + internal static TokenType[] AllowedEscapedIdentifierNames => allowedEscapedIdentifierNames; + + internal static TokenType[] AllowedUnescapedIdentifierNames => allowedUnescapedIdentifierNames; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Tokens/Token.cs b/src/Rules.Framework.Rql/Tokens/Token.cs new file mode 100644 index 00000000..0c5fd343 --- /dev/null +++ b/src/Rules.Framework.Rql/Tokens/Token.cs @@ -0,0 +1,49 @@ +namespace Rules.Framework.Rql.Tokens +{ + using System; + using Rules.Framework.Rql; + + internal class Token + { + private Token(string lexeme, bool isEscaped, object literal, RqlSourcePosition beginPosition, RqlSourcePosition endPosition, uint length, TokenType type) + { + this.Length = length; + this.Lexeme = lexeme; + this.IsEscaped = isEscaped; + this.Literal = literal; + this.BeginPosition = beginPosition; + this.EndPosition = endPosition; + this.Type = type; + } + + public static Token None { get; } = new Token(lexeme: null, isEscaped: false, literal: null, RqlSourcePosition.Empty, RqlSourcePosition.Empty, 0, TokenType.None); + + public RqlSourcePosition BeginPosition { get; } + + public RqlSourcePosition EndPosition { get; } + + public bool IsEscaped { get; } + + public uint Length { get; } + + public string Lexeme { get; } + + public object Literal { get; } + + public TokenType Type { get; } + + public string UnescapedLexeme => this.IsEscaped ? this.Lexeme.Substring(1, this.Lexeme.Length - 1) : this.Lexeme; + + public static Token Create(string lexeme, bool isEscaped, object literal, RqlSourcePosition beginPosition, RqlSourcePosition endPosition, uint length, TokenType type) + { + if (lexeme is null) + { + throw new ArgumentNullException(nameof(lexeme), $"'{nameof(lexeme)}' cannot be null."); + } + + return new Token(lexeme, isEscaped, literal, beginPosition, endPosition, length, type); + } + + public override string ToString() => $"[{this.Type}] {this.Lexeme}: {this.Literal} @{this.BeginPosition},{this.EndPosition}"; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Tokens/TokenType.cs b/src/Rules.Framework.Rql/Tokens/TokenType.cs new file mode 100644 index 00000000..2a2c15e1 --- /dev/null +++ b/src/Rules.Framework.Rql/Tokens/TokenType.cs @@ -0,0 +1,168 @@ +namespace Rules.Framework.Rql.Tokens +{ + internal enum TokenType + { + None = 0, + + #region Keywords + + [AllowAsIdentifier(RequireEscaping = true)] + ACTIVATE, + + [AllowAsIdentifier] + ALL, + + [AllowAsIdentifier] + AND, + + [AllowAsIdentifier] + APPLY, + + [AllowAsIdentifier(RequireEscaping = true)] + ARRAY, + + [AllowAsIdentifier] + AS, + + [AllowAsIdentifier] + BOTTOM, + + [AllowAsIdentifier] + CONTENT, + + [AllowAsIdentifier(RequireEscaping = true)] + CREATE, + + [AllowAsIdentifier] + DEACTIVATE, + + [AllowAsIdentifier(RequireEscaping = true)] + ELSE, + + [AllowAsIdentifier(RequireEscaping = true)] + FOR, + + [AllowAsIdentifier(RequireEscaping = true)] + FOREACH, + + [AllowAsIdentifier(RequireEscaping = true)] + IF, + + [AllowAsIdentifier] + IS, + + [AllowAsIdentifier(RequireEscaping = true)] + MATCH, + + [AllowAsIdentifier] + NAME, + + [AllowAsIdentifier(RequireEscaping = true)] + NOTHING, + + [AllowAsIdentifier] + NUMBER, + + [AllowAsIdentifier(RequireEscaping = true)] + OBJECT, + + [AllowAsIdentifier] + ON, + + [AllowAsIdentifier] + ONE, + + [AllowAsIdentifier] + OR, + + [AllowAsIdentifier] + PRIORITY, + + [AllowAsIdentifier] + RULE, + + [AllowAsIdentifier] + RULES, + + [AllowAsIdentifier(RequireEscaping = true)] + SEARCH, + + [AllowAsIdentifier] + SET, + + [AllowAsIdentifier] + SINCE, + + [AllowAsIdentifier] + TO, + + [AllowAsIdentifier] + TOP, + + [AllowAsIdentifier] + UNTIL, + + [AllowAsIdentifier(RequireEscaping = true)] + UPDATE, + + [AllowAsIdentifier(RequireEscaping = true)] + VAR, + + [AllowAsIdentifier] + WHEN, + + [AllowAsIdentifier] + WITH, + + #endregion Keywords + + #region Literals + + BOOL, + DATE, + DECIMAL, + + [AllowAsIdentifier] + IDENTIFIER, + + INT, + PLACEHOLDER, + STRING, + + #endregion Literals + + #region Operators + + ASSIGN, + EQUAL, + GREATER_THAN, + GREATER_THAN_OR_EQUAL, + LESS_THAN, + LESS_THAN_OR_EQUAL, + IN, + MINUS, + STAR, + NOT_EQUAL, + NOT, + PLUS, + SLASH, + + #endregion Operators + + #region Tokens + + BRACE_LEFT, + BRACE_RIGHT, + BRACKET_LEFT, + BRACKET_RIGHT, + COMMA, + DOT, + ESCAPE, + SEMICOLON, + STRAIGHT_BRACKET_LEFT, + STRAIGHT_BRACKET_RIGHT, + EOF, + + #endregion Tokens + } +} \ No newline at end of file diff --git a/src/Rules.Framework.Rql/ValueResult.cs b/src/Rules.Framework.Rql/ValueResult.cs new file mode 100644 index 00000000..ecbc7392 --- /dev/null +++ b/src/Rules.Framework.Rql/ValueResult.cs @@ -0,0 +1,17 @@ +namespace Rules.Framework.Rql +{ + using System.Diagnostics.CodeAnalysis; + + [ExcludeFromCodeCoverage] + public class ValueResult : IResult + { + public ValueResult(string rql, object value) + { + this.Rql = rql; + this.Value = value; + } + + public string Rql { get; } + public object Value { get; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework/IRulesEngine.cs b/src/Rules.Framework/IRulesEngine.cs index 5b29c9ea..cfa90152 100644 --- a/src/Rules.Framework/IRulesEngine.cs +++ b/src/Rules.Framework/IRulesEngine.cs @@ -5,7 +5,7 @@ namespace Rules.Framework using System.Threading.Tasks; using Rules.Framework.Core; - internal interface IRulesEngine + public interface IRulesEngine { Task AddRuleAsync(Rule rule, RuleAddPriorityOption ruleAddPriorityOption); diff --git a/tests/Rules.Framework.BenchmarkTests/AssemblyMetadata.cs b/tests/Rules.Framework.BenchmarkTests/AssemblyMetadata.cs new file mode 100644 index 00000000..94c43b99 --- /dev/null +++ b/tests/Rules.Framework.BenchmarkTests/AssemblyMetadata.cs @@ -0,0 +1,3 @@ +using System.Diagnostics.CodeAnalysis; + +[assembly: ExcludeFromCodeCoverage] \ No newline at end of file diff --git a/tests/Rules.Framework.BenchmarkTests/Rules.Framework.BenchmarkTests.csproj b/tests/Rules.Framework.BenchmarkTests/Rules.Framework.BenchmarkTests.csproj index c6a32a4a..fd99834a 100644 --- a/tests/Rules.Framework.BenchmarkTests/Rules.Framework.BenchmarkTests.csproj +++ b/tests/Rules.Framework.BenchmarkTests/Rules.Framework.BenchmarkTests.csproj @@ -15,7 +15,7 @@ - + diff --git a/tests/Rules.Framework.BenchmarkTests/Tests/Benchmark3/Benchmark3.cs b/tests/Rules.Framework.BenchmarkTests/Tests/Benchmark3/Benchmark3.cs index 900f0e91..664a4c26 100644 --- a/tests/Rules.Framework.BenchmarkTests/Tests/Benchmark3/Benchmark3.cs +++ b/tests/Rules.Framework.BenchmarkTests/Tests/Benchmark3/Benchmark3.cs @@ -2,6 +2,7 @@ namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 { using System.Threading.Tasks; using BenchmarkDotNet.Attributes; + using Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8; [SkewnessColumn, KurtosisColumn] public class Benchmark3 : IBenchmark diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/IScenarioData.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/IScenarioData.cs index 3d080c4c..12458f58 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/IScenarioData.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/IScenarioData.cs @@ -1,4 +1,4 @@ -namespace Rules.Framework.BenchmarkTests.Tests +namespace Rules.Framework.IntegrationTests.Common.Scenarios { using System; using System.Collections.Generic; diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario6/Scenario6Data.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario6/Scenario6Data.cs index 430e9183..49b37d23 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario6/Scenario6Data.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario6/Scenario6Data.cs @@ -2,8 +2,8 @@ namespace Rules.Framework.BenchmarkTests.Tests.Benchmark1 { using System; using System.Collections.Generic; - using Rules.Framework.BenchmarkTests.Tests; using Rules.Framework.Core; + using Rules.Framework.IntegrationTests.Common.Scenarios; public class Scenario6Data : IScenarioData { diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario7/Scenario7Data.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario7/Scenario7Data.cs index 028e4b6e..70dc37fe 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario7/Scenario7Data.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario7/Scenario7Data.cs @@ -4,6 +4,7 @@ namespace Rules.Framework.BenchmarkTests.Tests.Benchmark2 using System.Collections.Generic; using Rules.Framework; using Rules.Framework.Core; + using Rules.Framework.IntegrationTests.Common.Scenarios; public class Scenario7Data : IScenarioData { diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/ConditionTypes.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/ConditionTypes.cs index 4f478167..99dcdc05 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/ConditionTypes.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/ConditionTypes.cs @@ -1,4 +1,4 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { public enum ConditionTypes { diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/ContentTypes.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/ContentTypes.cs index 48432a66..9e0fa682 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/ContentTypes.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/ContentTypes.cs @@ -1,4 +1,4 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { public enum ContentTypes { diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Flush.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Flush.cs index 2efa715e..dc0cdf24 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Flush.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Flush.cs @@ -1,8 +1,7 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { using System; using System.Collections.Generic; - using Rules.Framework.Builder; using Rules.Framework.Core; public partial class Scenario8Data : IScenarioData @@ -12,25 +11,25 @@ private IEnumerable> GetFlushRules() return new[] { RuleBuilder.NewRule() - .WithName("Benchmark 3 - Flush of Clubs") + .WithName("Scenario 8 - Flush of Clubs") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Flush" }) .WithCondition(ConditionTypes.NumberOfClubs, Operators.GreaterThanOrEqual, 5) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Flush of Diamonds") + .WithName("Scenario 8 - Flush of Diamonds") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Flush" }) .WithCondition(ConditionTypes.NumberOfDiamonds, Operators.GreaterThanOrEqual, 5) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Flush of Hearts") + .WithName("Scenario 8 - Flush of Hearts") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Flush" }) .WithCondition(ConditionTypes.NumberOfHearts, Operators.GreaterThanOrEqual, 5) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Flush of Spades") + .WithName("Scenario 8 - Flush of Spades") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Flush" }) .WithCondition(ConditionTypes.NumberOfSpades, Operators.GreaterThanOrEqual, 5) diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.FourOfAKind.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.FourOfAKind.cs index fcbbc45a..096e9d96 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.FourOfAKind.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.FourOfAKind.cs @@ -1,8 +1,7 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { using System; using System.Collections.Generic; - using Rules.Framework.Builder; using Rules.Framework.Core; public partial class Scenario8Data : IScenarioData @@ -12,79 +11,79 @@ private IEnumerable> GetFourOfAKindRules() return new[] { RuleBuilder.NewRule() - .WithName("Benchmark 3 - Four Of A Kind Deuces") + .WithName("Scenario 8 - Four Of A Kind Deuces") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .WithCondition(ConditionTypes.NumberOfDeuces, Operators.Equal, 4) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Four Of A Kind Treys") + .WithName("Scenario 8 - Four Of A Kind Treys") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .WithCondition(ConditionTypes.NumberOfTreys, Operators.Equal, 4) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Four Of A Kind Fours") + .WithName("Scenario 8 - Four Of A Kind Fours") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .WithCondition(ConditionTypes.NumberOfFours, Operators.Equal, 4) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Four Of A Kind Fives") + .WithName("Scenario 8 - Four Of A Kind Fives") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .WithCondition(ConditionTypes.NumberOfFives, Operators.Equal, 4) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Four Of A Kind Sixes") + .WithName("Scenario 8 - Four Of A Kind Sixes") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .WithCondition(ConditionTypes.NumberOfSixes, Operators.Equal, 4) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Four Of A Kind Sevens") + .WithName("Scenario 8 - Four Of A Kind Sevens") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .WithCondition(ConditionTypes.NumberOfSevens, Operators.Equal, 4) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Four Of A Kind Eights") + .WithName("Scenario 8 - Four Of A Kind Eights") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .WithCondition(ConditionTypes.NumberOfEigths, Operators.Equal, 4) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Four Of A Kind Nines") + .WithName("Scenario 8 - Four Of A Kind Nines") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .WithCondition(ConditionTypes.NumberOfNines, Operators.Equal, 4) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Four Of A Kind Tens") + .WithName("Scenario 8 - Four Of A Kind Tens") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .WithCondition(ConditionTypes.NumberOfTens, Operators.Equal, 4) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Four Of A Kind Jacks") + .WithName("Scenario 8 - Four Of A Kind Jacks") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .WithCondition(ConditionTypes.NumberOfJacks, Operators.Equal, 4) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Four Of A Kind Queens") + .WithName("Scenario 8 - Four Of A Kind Queens") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .WithCondition(ConditionTypes.NumberOfQueens, Operators.Equal, 4) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Four Of A Kind Kings") + .WithName("Scenario 8 - Four Of A Kind Kings") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .WithCondition(ConditionTypes.NumberOfKings, Operators.Equal, 4) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Four Of A Kind Aces") + .WithName("Scenario 8 - Four Of A Kind Aces") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Four Of A Kind" }) .WithCondition(ConditionTypes.NumberOfAces, Operators.Equal, 4) diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.HighCard.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.HighCard.cs index d1851617..161f9d7a 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.HighCard.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.HighCard.cs @@ -1,8 +1,7 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { using System; using System.Collections.Generic; - using Rules.Framework.Builder; using Rules.Framework.Core; public partial class Scenario8Data : IScenarioData @@ -12,79 +11,79 @@ private IEnumerable> GetHighCardsRules() return new[] { RuleBuilder.NewRule() - .WithName("Benchmark 3 - High Card Deuces") + .WithName("Scenario 8 - High Card Deuces") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "High Card" }) .WithCondition(ConditionTypes.NumberOfDeuces, Operators.Equal, 1) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - High Card Treys") + .WithName("Scenario 8 - High Card Treys") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "High Card" }) .WithCondition(ConditionTypes.NumberOfTreys, Operators.Equal, 1) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - High Card Fours") + .WithName("Scenario 8 - High Card Fours") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "High Card" }) .WithCondition(ConditionTypes.NumberOfFours, Operators.Equal, 1) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - High Card Fives") + .WithName("Scenario 8 - High Card Fives") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "High Card" }) .WithCondition(ConditionTypes.NumberOfFives, Operators.Equal, 1) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - High Card Sixes") + .WithName("Scenario 8 - High Card Sixes") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "High Card" }) .WithCondition(ConditionTypes.NumberOfSixes, Operators.Equal, 1) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - High Card Sevens") + .WithName("Scenario 8 - High Card Sevens") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "High Card" }) .WithCondition(ConditionTypes.NumberOfSevens, Operators.Equal, 1) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - High Card Eights") + .WithName("Scenario 8 - High Card Eights") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "High Card" }) .WithCondition(ConditionTypes.NumberOfEigths, Operators.Equal, 1) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - High Card Nines") + .WithName("Scenario 8 - High Card Nines") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "High Card" }) .WithCondition(ConditionTypes.NumberOfNines, Operators.Equal, 1) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - High Card Tens") + .WithName("Scenario 8 - High Card Tens") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "High Card" }) .WithCondition(ConditionTypes.NumberOfTens, Operators.Equal, 1) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - High Card Jacks") + .WithName("Scenario 8 - High Card Jacks") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "High Card" }) .WithCondition(ConditionTypes.NumberOfJacks, Operators.Equal, 1) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - High Card Queens") + .WithName("Scenario 8 - High Card Queens") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "High Card" }) .WithCondition(ConditionTypes.NumberOfQueens, Operators.Equal, 1) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - High Card Kings") + .WithName("Scenario 8 - High Card Kings") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "High Card" }) .WithCondition(ConditionTypes.NumberOfKings, Operators.Equal, 1) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - High Card Aces") + .WithName("Scenario 8 - High Card Aces") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "High Card" }) .WithCondition(ConditionTypes.NumberOfAces, Operators.Equal, 1) diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Pair.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Pair.cs index 72649fd8..1d5f166d 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Pair.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Pair.cs @@ -1,8 +1,7 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { using System; using System.Collections.Generic; - using Rules.Framework.Builder; using Rules.Framework.Core; public partial class Scenario8Data : IScenarioData @@ -12,79 +11,79 @@ private IEnumerable> GetPairsRules() return new[] { RuleBuilder.NewRule() - .WithName("Benchmark 3 - Pair Deuces") + .WithName("Scenario 8 - Pair Deuces") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Pair" }) .WithCondition(ConditionTypes.NumberOfDeuces, Operators.Equal, 2) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Pair Treys") + .WithName("Scenario 8 - Pair Treys") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Pair" }) .WithCondition(ConditionTypes.NumberOfTreys, Operators.Equal, 2) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Pair Fours") + .WithName("Scenario 8 - Pair Fours") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Pair" }) .WithCondition(ConditionTypes.NumberOfFours, Operators.Equal, 2) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Pair Fives") + .WithName("Scenario 8 - Pair Fives") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Pair" }) .WithCondition(ConditionTypes.NumberOfFives, Operators.Equal, 2) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Pair Sixes") + .WithName("Scenario 8 - Pair Sixes") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Pair" }) .WithCondition(ConditionTypes.NumberOfSixes, Operators.Equal, 2) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Pair Sevens") + .WithName("Scenario 8 - Pair Sevens") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Pair" }) .WithCondition(ConditionTypes.NumberOfSevens, Operators.Equal, 2) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Pair Eights") + .WithName("Scenario 8 - Pair Eights") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Pair" }) .WithCondition(ConditionTypes.NumberOfEigths, Operators.Equal, 2) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Pair Nines") + .WithName("Scenario 8 - Pair Nines") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Pair" }) .WithCondition(ConditionTypes.NumberOfNines, Operators.Equal, 2) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Pair Tens") + .WithName("Scenario 8 - Pair Tens") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Pair" }) .WithCondition(ConditionTypes.NumberOfTens, Operators.Equal, 2) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Pair Jacks") + .WithName("Scenario 8 - Pair Jacks") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Pair" }) .WithCondition(ConditionTypes.NumberOfJacks, Operators.Equal, 2) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Pair Queens") + .WithName("Scenario 8 - Pair Queens") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Pair" }) .WithCondition(ConditionTypes.NumberOfQueens, Operators.Equal, 2) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Pair Kings") + .WithName("Scenario 8 - Pair Kings") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Pair" }) .WithCondition(ConditionTypes.NumberOfKings, Operators.Equal, 2) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Pair Aces") + .WithName("Scenario 8 - Pair Aces") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Pair" }) .WithCondition(ConditionTypes.NumberOfAces, Operators.Equal, 2) diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.RoyalFlush.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.RoyalFlush.cs index 57297782..ae9aef8e 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.RoyalFlush.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.RoyalFlush.cs @@ -1,4 +1,4 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { using System; using System.Collections.Generic; @@ -11,7 +11,7 @@ private IEnumerable> GetRoyalFlushRules() return new[] { RuleBuilder.NewRule() - .WithName("Benchmark 3 - Royal flush of Clubs: Ace, King, Queen, Jack, 10") + .WithName("Scenario 8 - Royal flush of Clubs: Ace, King, Queen, Jack, 10") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Royal Flush" }) .WithCondition(c => c @@ -25,7 +25,7 @@ private IEnumerable> GetRoyalFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Royal flush of Diamonds: Ace, King, Queen, Jack, 10") + .WithName("Scenario 8 - Royal flush of Diamonds: Ace, King, Queen, Jack, 10") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Royal Flush" }) .WithCondition(c => c @@ -39,7 +39,7 @@ private IEnumerable> GetRoyalFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Royal flush of Hearts: Ace, King, Queen, Jack, 10") + .WithName("Scenario 8 - Royal flush of Hearts: Ace, King, Queen, Jack, 10") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Royal Flush" }) .WithCondition(c => c @@ -53,7 +53,7 @@ private IEnumerable> GetRoyalFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Royal flush of Spades: Ace, King, Queen, Jack, 10") + .WithName("Scenario 8 - Royal flush of Spades: Ace, King, Queen, Jack, 10") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Royal Flush" }) .WithCondition(c => c diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Straight.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Straight.cs index 8cad58e1..90a71f3f 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Straight.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.Straight.cs @@ -1,4 +1,4 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { using System; using System.Collections.Generic; @@ -11,7 +11,7 @@ private IEnumerable> GetStraightRules() return new[] { RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight 6, 5, 4, 3, 2") + .WithName("Scenario 8 - Straight 6, 5, 4, 3, 2") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight" }) .WithCondition(c => c @@ -25,7 +25,7 @@ private IEnumerable> GetStraightRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight 7, 6, 5, 4, 3") + .WithName("Scenario 8 - Straight 7, 6, 5, 4, 3") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight" }) .WithCondition(c => c @@ -39,7 +39,7 @@ private IEnumerable> GetStraightRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight 8, 7, 6, 5, 4") + .WithName("Scenario 8 - Straight 8, 7, 6, 5, 4") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight" }) .WithCondition(c => c @@ -53,7 +53,7 @@ private IEnumerable> GetStraightRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight 9, 8, 7, 6, 5") + .WithName("Scenario 8 - Straight 9, 8, 7, 6, 5") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight" }) .WithCondition(c => c @@ -67,7 +67,7 @@ private IEnumerable> GetStraightRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight 10, 9, 8, 7, 6") + .WithName("Scenario 8 - Straight 10, 9, 8, 7, 6") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight" }) .WithCondition(c => c @@ -81,7 +81,7 @@ private IEnumerable> GetStraightRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight Jack, 10, 9, 8, 7") + .WithName("Scenario 8 - Straight Jack, 10, 9, 8, 7") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight" }) .WithCondition(c => c @@ -95,7 +95,7 @@ private IEnumerable> GetStraightRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight Queen, Jack, 10, 9, 8") + .WithName("Scenario 8 - Straight Queen, Jack, 10, 9, 8") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight" }) .WithCondition(c => c @@ -109,7 +109,7 @@ private IEnumerable> GetStraightRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight King, Queen, Jack, 10, 9") + .WithName("Scenario 8 - Straight King, Queen, Jack, 10, 9") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight" }) .WithCondition(c => c diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.StraightFlush.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.StraightFlush.cs index 2cbcb60a..4698d923 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.StraightFlush.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.StraightFlush.cs @@ -1,8 +1,7 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { using System; using System.Collections.Generic; - using Rules.Framework.Builder; using Rules.Framework.Core; public partial class Scenario8Data : IScenarioData @@ -13,7 +12,7 @@ private IEnumerable> GetStraightFlushRules() { // Straight flush of Clubs: RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Clubs: 6, 5, 4, 3, 2") + .WithName("Scenario 8 - Straight flush of Clubs: 6, 5, 4, 3, 2") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -27,7 +26,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Clubs: 7, 6, 5, 4, 3") + .WithName("Scenario 8 - Straight flush of Clubs: 7, 6, 5, 4, 3") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -41,7 +40,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Clubs: 8, 7, 6, 5, 4") + .WithName("Scenario 8 - Straight flush of Clubs: 8, 7, 6, 5, 4") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -55,7 +54,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Clubs: 9, 8, 7, 6, 5") + .WithName("Scenario 8 - Straight flush of Clubs: 9, 8, 7, 6, 5") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -69,7 +68,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Clubs: 10, 9, 8, 7, 6") + .WithName("Scenario 8 - Straight flush of Clubs: 10, 9, 8, 7, 6") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -83,7 +82,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Clubs: Jack, 10, 9, 8, 7") + .WithName("Scenario 8 - Straight flush of Clubs: Jack, 10, 9, 8, 7") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -97,7 +96,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Clubs: Queen, Jack, 10, 9, 8") + .WithName("Scenario 8 - Straight flush of Clubs: Queen, Jack, 10, 9, 8") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -111,7 +110,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Clubs: King, Queen, Jack, 10, 9") + .WithName("Scenario 8 - Straight flush of Clubs: King, Queen, Jack, 10, 9") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -127,7 +126,7 @@ private IEnumerable> GetStraightFlushRules() // Straight flush of Diamonds: RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Diamonds: 6, 5, 4, 3, 2") + .WithName("Scenario 8 - Straight flush of Diamonds: 6, 5, 4, 3, 2") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -141,7 +140,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Diamonds: 7, 6, 5, 4, 3") + .WithName("Scenario 8 - Straight flush of Diamonds: 7, 6, 5, 4, 3") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -155,7 +154,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Diamonds: 8, 7, 6, 5, 4") + .WithName("Scenario 8 - Straight flush of Diamonds: 8, 7, 6, 5, 4") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -169,7 +168,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Diamonds: 9, 8, 7, 6, 5") + .WithName("Scenario 8 - Straight flush of Diamonds: 9, 8, 7, 6, 5") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -183,7 +182,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Diamonds: 10, 9, 8, 7, 6") + .WithName("Scenario 8 - Straight flush of Diamonds: 10, 9, 8, 7, 6") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -197,7 +196,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Diamonds: Jack, 10, 9, 8, 7") + .WithName("Scenario 8 - Straight flush of Diamonds: Jack, 10, 9, 8, 7") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -211,7 +210,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Diamonds: Queen, Jack, 10, 9, 8") + .WithName("Scenario 8 - Straight flush of Diamonds: Queen, Jack, 10, 9, 8") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -225,7 +224,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Diamonds: King, Queen, Jack, 10, 9") + .WithName("Scenario 8 - Straight flush of Diamonds: King, Queen, Jack, 10, 9") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -241,7 +240,7 @@ private IEnumerable> GetStraightFlushRules() // Straight flush of Hearts: RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Hearts: 6, 5, 4, 3, 2") + .WithName("Scenario 8 - Straight flush of Hearts: 6, 5, 4, 3, 2") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -255,7 +254,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Hearts: 7, 6, 5, 4, 3") + .WithName("Scenario 8 - Straight flush of Hearts: 7, 6, 5, 4, 3") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -269,7 +268,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Hearts: 8, 7, 6, 5, 4") + .WithName("Scenario 8 - Straight flush of Hearts: 8, 7, 6, 5, 4") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -283,7 +282,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Hearts: 9, 8, 7, 6, 5") + .WithName("Scenario 8 - Straight flush of Hearts: 9, 8, 7, 6, 5") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -297,7 +296,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Hearts: 10, 9, 8, 7, 6") + .WithName("Scenario 8 - Straight flush of Hearts: 10, 9, 8, 7, 6") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -311,7 +310,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Hearts: Jack, 10, 9, 8, 7") + .WithName("Scenario 8 - Straight flush of Hearts: Jack, 10, 9, 8, 7") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -325,7 +324,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Hearts: Queen, Jack, 10, 9, 8") + .WithName("Scenario 8 - Straight flush of Hearts: Queen, Jack, 10, 9, 8") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -339,7 +338,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Hearts: King, Queen, Jack, 10, 9") + .WithName("Scenario 8 - Straight flush of Hearts: King, Queen, Jack, 10, 9") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -355,7 +354,7 @@ private IEnumerable> GetStraightFlushRules() // Straight flush of Spades: RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Spades: 6, 5, 4, 3, 2") + .WithName("Scenario 8 - Straight flush of Spades: 6, 5, 4, 3, 2") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -369,7 +368,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Spades: 7, 6, 5, 4, 3") + .WithName("Scenario 8 - Straight flush of Spades: 7, 6, 5, 4, 3") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -383,7 +382,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Spades: 8, 7, 6, 5, 4") + .WithName("Scenario 8 - Straight flush of Spades: 8, 7, 6, 5, 4") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -397,7 +396,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Spades: 9, 8, 7, 6, 5") + .WithName("Scenario 8 - Straight flush of Spades: 9, 8, 7, 6, 5") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -411,7 +410,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Spades: 10, 9, 8, 7, 6") + .WithName("Scenario 8 - Straight flush of Spades: 10, 9, 8, 7, 6") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -425,7 +424,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Spades: Jack, 10, 9, 8, 7") + .WithName("Scenario 8 - Straight flush of Spades: Jack, 10, 9, 8, 7") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -439,7 +438,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Spades: Queen, Jack, 10, 9, 8") + .WithName("Scenario 8 - Straight flush of Spades: Queen, Jack, 10, 9, 8") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c @@ -453,7 +452,7 @@ private IEnumerable> GetStraightFlushRules() ) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Straight flush of Spades: King, Queen, Jack, 10, 9") + .WithName("Scenario 8 - Straight flush of Spades: King, Queen, Jack, 10, 9") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Straight Flush" }) .WithCondition(c => c diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.ThreeOfAKind.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.ThreeOfAKind.cs index 637c3cb4..3668b939 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.ThreeOfAKind.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.ThreeOfAKind.cs @@ -1,8 +1,7 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { using System; using System.Collections.Generic; - using Rules.Framework.Builder; using Rules.Framework.Core; public partial class Scenario8Data : IScenarioData @@ -12,79 +11,79 @@ private IEnumerable> GetThreeOfAKindRules() return new[] { RuleBuilder.NewRule() - .WithName("Benchmark 3 - Three Of A Kind Deuces") + .WithName("Scenario 8 - Three Of A Kind Deuces") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .WithCondition(ConditionTypes.NumberOfDeuces, Operators.Equal, 3) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Three Of A Kind Treys") + .WithName("Scenario 8 - Three Of A Kind Treys") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .WithCondition(ConditionTypes.NumberOfTreys, Operators.Equal, 3) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Three Of A Kind Fours") + .WithName("Scenario 8 - Three Of A Kind Fours") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) - .WithCondition(ConditionTypes.NumberOfDeuces, Operators.Equal, 3) + .WithCondition(ConditionTypes.NumberOfFours, Operators.Equal, 3) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Three Of A Kind Fives") + .WithName("Scenario 8 - Three Of A Kind Fives") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .WithCondition(ConditionTypes.NumberOfFives, Operators.Equal, 3) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Three Of A Kind Sixes") + .WithName("Scenario 8 - Three Of A Kind Sixes") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .WithCondition(ConditionTypes.NumberOfSixes, Operators.Equal, 3) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Three Of A Kind Sevens") + .WithName("Scenario 8 - Three Of A Kind Sevens") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .WithCondition(ConditionTypes.NumberOfSevens, Operators.Equal, 3) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Three Of A Kind Eights") + .WithName("Scenario 8 - Three Of A Kind Eights") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .WithCondition(ConditionTypes.NumberOfEigths, Operators.Equal, 3) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Three Of A Kind Nines") + .WithName("Scenario 8 - Three Of A Kind Nines") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .WithCondition(ConditionTypes.NumberOfNines, Operators.Equal, 3) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Three Of A Kind Tens") + .WithName("Scenario 8 - Three Of A Kind Tens") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .WithCondition(ConditionTypes.NumberOfTens, Operators.Equal, 3) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Three Of A Kind Jacks") + .WithName("Scenario 8 - Three Of A Kind Jacks") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .WithCondition(ConditionTypes.NumberOfJacks, Operators.Equal, 3) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Three Of A Kind Queens") + .WithName("Scenario 8 - Three Of A Kind Queens") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .WithCondition(ConditionTypes.NumberOfQueens, Operators.Equal, 3) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Three Of A Kind Kings") + .WithName("Scenario 8 - Three Of A Kind Kings") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .WithCondition(ConditionTypes.NumberOfKings, Operators.Equal, 3) .Build().Rule, RuleBuilder.NewRule() - .WithName("Benchmark 3 - Three Of A Kind Aces") + .WithName("Scenario 8 - Three Of A Kind Aces") .WithDateBegin(DateTime.Parse("2000-01-01")) .WithContent(ContentTypes.TexasHoldemPokerSingleCombinations, new SingleCombinationPokerScore { Combination = "Three Of A Kind" }) .WithCondition(ConditionTypes.NumberOfAces, Operators.Equal, 3) diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.cs index 47c1eb16..5568ba9e 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/Scenario8Data.cs @@ -1,4 +1,4 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { using System; using System.Collections.Generic; diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/CardPokerScore.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/SingleCombinationPokerScore.cs similarity index 68% rename from tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/CardPokerScore.cs rename to tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/SingleCombinationPokerScore.cs index 5ee3874a..4020e019 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/CardPokerScore.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/Scenario8/SingleCombinationPokerScore.cs @@ -1,4 +1,4 @@ -namespace Rules.Framework.BenchmarkTests.Tests.Benchmark3 +namespace Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8 { public class SingleCombinationPokerScore { diff --git a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/ScenarioLoader.cs b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/ScenarioLoader.cs index df2e64a2..92eecd4e 100644 --- a/tests/Rules.Framework.IntegrationTests.Common/Scenarios/ScenarioLoader.cs +++ b/tests/Rules.Framework.IntegrationTests.Common/Scenarios/ScenarioLoader.cs @@ -1,11 +1,9 @@ namespace Rules.Framework.IntegrationTests.Common.Scenarios { - using Rules.Framework.BenchmarkTests.Tests; - public static class ScenarioLoader { public static async Task LoadScenarioAsync( - RulesEngine rulesEngine, + IRulesEngine rulesEngine, IScenarioData scenarioData) { foreach (var rule in scenarioData.Rules) diff --git a/tests/Rules.Framework.IntegrationTests/Scenarios/Scenario8/TexasHoldEmPokerSingleCombinationsTests.cs b/tests/Rules.Framework.IntegrationTests/Scenarios/Scenario8/TexasHoldEmPokerSingleCombinationsTests.cs index 8d9c7e29..ba978523 100644 --- a/tests/Rules.Framework.IntegrationTests/Scenarios/Scenario8/TexasHoldEmPokerSingleCombinationsTests.cs +++ b/tests/Rules.Framework.IntegrationTests/Scenarios/Scenario8/TexasHoldEmPokerSingleCombinationsTests.cs @@ -3,8 +3,8 @@ namespace Rules.Framework.IntegrationTests.Scenarios.Scenario8 using System; using System.Threading.Tasks; using FluentAssertions; - using Rules.Framework.BenchmarkTests.Tests.Benchmark3; using Rules.Framework.IntegrationTests.Common.Scenarios; + using Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8; using Xunit; public class TexasHoldEmPokerSingleCombinationsTests diff --git a/tests/Rules.Framework.Rql.IntegrationTests/AssemblyMetadata.cs b/tests/Rules.Framework.Rql.IntegrationTests/AssemblyMetadata.cs new file mode 100644 index 00000000..df17c94d --- /dev/null +++ b/tests/Rules.Framework.Rql.IntegrationTests/AssemblyMetadata.cs @@ -0,0 +1,8 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Xunit; + +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +[assembly: ExcludeFromCodeCoverage] +[assembly: AssemblyTrait("Category", "Integration")] +[assembly: CollectionBehavior(DisableTestParallelization = true)] \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/CheckFiles/BasicLanguageChecks.yaml b/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/CheckFiles/BasicLanguageChecks.yaml new file mode 100644 index 00000000..cf4f96ec --- /dev/null +++ b/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/CheckFiles/BasicLanguageChecks.yaml @@ -0,0 +1,160 @@ +checks: + - rql: 3; + expectsSuccess: true + expectedMessages: [] + - rql: -3; + expectsSuccess: true + expectedMessages: [] + - rql: 2a; + expectsSuccess: false + expectedMessages: + - Invalid number '2a'. + - rql: -2a; + expectsSuccess: false + expectedMessages: + - Invalid number '2a'. + - rql: 6.2; + expectsSuccess: true + expectedMessages: [] + - rql: -6.2; + expectsSuccess: true + expectedMessages: [] + - rql: 2.a; + expectsSuccess: false + expectedMessages: + - Invalid number '2.a'. + - rql: 2a.1a; + expectsSuccess: false + expectedMessages: + - Invalid number '2a.1a'. + - rql: nothing; + expectsSuccess: true + expectedMessages: [] + - rql: "\"some string\";" + expectsSuccess: true + expectedMessages: [] + - rql: "\"some string;" + expectsSuccess: false + expectedMessages: + - Unterminated string '"some string;'. + - rql: true; + expectsSuccess: true + expectedMessages: [] + - rql: false; + expectsSuccess: true + expectedMessages: [] + - rql: $2024-01-01$; + expectsSuccess: true + expectedMessages: [] + - rql: $2024-01-01; + expectsSuccess: false + expectedMessages: + - Unterminated date '$2024-01-01;'. + - rql: $2024$; + expectsSuccess: false + expectedMessages: + - Invalid date '$2024$'. + - rql: $2024-AA-BB$; + expectsSuccess: false + expectedMessages: + - Invalid date '$2024-AA-BB$'. + - rql: $$; + expectsSuccess: false + expectedMessages: + - Invalid date '$$'. + - rql: $2024-03-01T20:18:56Z$; + expectsSuccess: true + expectedMessages: [] + - rql: $2024-03-01T20:18:AAZ$; + expectsSuccess: false + expectedMessages: + - Invalid date '$2024-03-01T20:18:AAZ$'. + - rql: "{ 1, 2, 3 };" + expectsSuccess: true + expectedMessages: [] + - rql: "{ 1, 2, 3" + expectsSuccess: false + expectedMessages: + - Expected token '}'. + - rql: "{ 1, 2," + expectsSuccess: false + expectedMessages: + - Expected expression. + - rql: "{ }" + expectsSuccess: false + expectedMessages: + - Expected expression. + - rql: "{ 1, \"some string\", false, $2024-01-01$ };" + expectsSuccess: true + expectedMessages: [] + - rql: array[3]; + expectsSuccess: true + expectedMessages: [] + - rql: array + expectsSuccess: false + expectedMessages: + - Expected token '['. + - rql: array[] + expectsSuccess: false + expectedMessages: + - Expected integer literal. + - rql: array[ + expectsSuccess: false + expectedMessages: + - Expected integer literal. + - rql: array["abc" + expectsSuccess: false + expectedMessages: + - Expected integer literal. + - rql: array[5 + expectsSuccess: false + expectedMessages: + - Expected token ']'. + - rql: object; + expectsSuccess: true + expectedMessages: [] + - rql: object { }; + expectsSuccess: false + expectedMessages: + - Expected identifier for object property. + - rql: object { array }; + expectsSuccess: false + expectedMessages: + - Expected identifier for object property. + - rql: "object { #array };" + expectsSuccess: false + expectedMessages: + - Expected token '='. + - rql: "object { #array = match };" + expectsSuccess: false + expectedMessages: + - Expected expression. + - rql: "object { Prop = 1" + expectsSuccess: false + expectedMessages: + - Expected token '}'. + - rql: object { Prop1 = "sample value", Prop2 = true, Prop3 = 50, Prop4 = 31.7, Prop5 = $2024-01-01$, Prop6 = nothing }; + expectsSuccess: true + expectedMessages: [] + - rql: object { NestedObject1 = object { Prop = "Sample nested object" }, NestedObject2 = object, NestedArray1 = { 1, 2, 3 }, NestedArray2 = array[3] }; + expectsSuccess: true + expectedMessages: [] + - rql: object { Content = "sample content" }; + expectsSuccess: true + expectedMessages: [] + - rql: 1 + 1; + expectsSuccess: true + expectedMessages: [] + - rql: 3 - 1; + expectsSuccess: true + expectedMessages: [] + - rql: 3 * 2; + expectsSuccess: true + expectedMessages: [] + - rql: 12 / 6; + expectsSuccess: true + expectedMessages: [] + - rql: (2 + 5 - 3) * (-1 / 2); + expectsSuccess: false # to be supported in a future release, should be true when implemented + expectedMessages: + - Expected expression. \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/CheckFiles/MatchExpressionChecks.yaml b/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/CheckFiles/MatchExpressionChecks.yaml new file mode 100644 index 00000000..b7edaff8 --- /dev/null +++ b/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/CheckFiles/MatchExpressionChecks.yaml @@ -0,0 +1,113 @@ +checks: + - rql: "MATCH" + expectsSuccess: false + expectedMessages: + - Expected tokens 'ONE' or 'ALL'. + - rql: "MATCH ONE" + expectsSuccess: false + expectedMessages: + - Expected token 'RULE'. + - rql: "MATCH ALL" + expectsSuccess: false + expectedMessages: + - Expected token 'RULES'. + - rql: "MATCH ALL RULES" + expectsSuccess: false + expectedMessages: + - Expected token 'FOR'. + - rql: "MATCH ONE RULE FOR" + expectsSuccess: false + expectedMessages: + - Expected content type name. + - rql: MATCH ONE RULE FOR "Test Content" + expectsSuccess: false + expectedMessages: + - Expected token 'ON'. + - rql: MATCH ONE RULE FOR "Test Content" ON + expectsSuccess: false + expectedMessages: + - Expected literal of type date. + - rql: MATCH ONE RULE FOR "Test Content" ON $2020-01-01$; + expectsSuccess: true + expectedMessages: [] + - rql: MATCH ONE RULE FOR "Test Content" ON $2020-01-01$ + expectsSuccess: false + expectedMessages: + - Expected token ';'. + - rql: MATCH ONE RULE FOR "Test Content" ON "Test not a date" + expectsSuccess: false + expectedMessages: + - Expected literal of type date. + - rql: MATCH ONE RULE FOR "Test Content" ON 123 + expectsSuccess: false + expectedMessages: + - Expected literal of type date. + - rql: MATCH ONE RULE FOR "Test Content" ON 16.8 + expectsSuccess: false + expectedMessages: + - Expected literal of type date. + - rql: MATCH ONE RULE FOR "Test Content" ON false + expectsSuccess: false + expectedMessages: + - Expected literal of type date. + - rql: MATCH ONE RULE FOR "Test Content" ON nothing + expectsSuccess: false + expectedMessages: + - Expected literal of type date. + - rql: MATCH ONE RULE FOR 123 ON $2020-01-01$; + expectsSuccess: true + expectedMessages: [] + - rql: MATCH ONE RULE FOR true ON $2020-01-01$; + expectsSuccess: false + expectedMessages: + - Literal 'TRUE' is not allowed as a valid content type. Only literals of types [Integer, String] are allowed. + - rql: MATCH ONE RULE FOR 13.1 ON $2020-01-01$; + expectsSuccess: false + expectedMessages: + - Literal '13.1' is not allowed as a valid content type. Only literals of types [Integer, String] are allowed. + - rql: MATCH ONE RULE FOR NOTHING ON $2020-01-01$; + expectsSuccess: false + expectedMessages: + - Literal 'NOTHING' is not allowed as a valid content type. Only literals of types [Integer, String] are allowed. + - rql: MATCH ONE RULE FOR "Test Content" ON $2020-01-01$ WHEN { @SomeCondition is true }; + expectsSuccess: true + expectedMessages: [] + - rql: MATCH ONE RULE FOR "Test Content" ON $2020-01-01$ WHEN { @SomeCondition is true } + expectsSuccess: false + expectedMessages: + - Expected token ';'. + - rql: MATCH ONE RULE FOR "Test Content" ON $2020-01-01$ WHEN; + expectsSuccess: false + expectedMessages: + - Expected '{' after WITH. + - rql: MATCH ONE RULE FOR "Test Content" ON $2020-01-01$ WHEN {}; + expectsSuccess: false + expectedMessages: + - Expected placeholder (@) for condition. + - rql: MATCH ONE RULE FOR "Test Content" ON $2020-01-01$ WHEN { @SomeCondition }; + expectsSuccess: false + expectedMessages: + - Expected token 'IS'. + - rql: MATCH ONE RULE FOR "Test Content" ON $2020-01-01$ WHEN { @SomeCondition is }; + expectsSuccess: false + expectedMessages: + - Expected literal for condition. + - rql: MATCH ONE RULE FOR "Test Content" ON $2020-01-01$ WHEN { @SomeCondition is $2024-01-01$ }; + expectsSuccess: false + expectedMessages: + - Expected literal for condition. + - rql: MATCH ONE RULE FOR "Test Content" ON $2020-01-01$ WHEN { @SomeCondition is nothing }; + expectsSuccess: false + expectedMessages: + - Expected literal for condition. + - rql: MATCH ONE RULE FOR "Test Content" ON $2020-01-01$ WHEN { @SomeCondition is true, }; + expectsSuccess: false + expectedMessages: + - Expected placeholder (@) for condition. + - rql: MATCH ONE RULE FOR "Test Content" ON $2020-01-01$ WHEN { @SomeCondition is true + expectsSuccess: false + expectedMessages: + - Expected ',' or '}' after input condition. + - rql: MATCH ONE RULE FOR "Test Content" ON $2020-01-01$ WHEN { @SomeCondition is true, @OtherCondition is 123, @AnotherCondition is 123.45, @YetAnotherCondition is "Test" }; + expectsSuccess: true + expectedMessages: [] \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/CheckFiles/SearchExpressionChecks.yaml b/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/CheckFiles/SearchExpressionChecks.yaml new file mode 100644 index 00000000..f206f518 --- /dev/null +++ b/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/CheckFiles/SearchExpressionChecks.yaml @@ -0,0 +1,121 @@ +checks: + - rql: SEARCH + expectsSuccess: false + expectedMessages: + - Expected token 'RULES'. + - rql: SEARCH RULE + expectsSuccess: false + expectedMessages: + - Expected token 'RULES'. + - rql: SEARCH RULES + expectsSuccess: false + expectedMessages: + - Expected token 'FOR'. + - rql: SEARCH RULES FOR + expectsSuccess: false + expectedMessages: + - Expected content type name. + - rql: SEARCH RULES FOR "Test Content" + expectsSuccess: false + expectedMessages: + - Expected token 'SINCE'. + - rql: SEARCH RULES FOR "Test Content" SINCE + expectsSuccess: false + expectedMessages: + - Expected literal of type date. + - rql: SEARCH RULES FOR "Test Content" SINCE $2020-01-01$; + expectsSuccess: false + expectedMessages: + - Expected token 'UNTIL'. + - rql: SEARCH RULES FOR "Test Content" SINCE $2020-01-01$ UNTIL + expectsSuccess: false + expectedMessages: + - Expected literal of type date. + - rql: SEARCH RULES FOR "Test Content" SINCE $2020-01-01$ UNTIL $2021-01-01$; + expectsSuccess: true + expectedMessages: [] + - rql: SEARCH RULES FOR "Test Content" SINCE "Test not a date" + expectsSuccess: false + expectedMessages: + - Expected literal of type date. + - rql: SEARCH RULES FOR "Test Content" SINCE $2020-01-01$ UNTIL "Test not a date" + expectsSuccess: false + expectedMessages: + - Expected literal of type date. + - rql: SEARCH RULES FOR "Test Content" SINCE 123 + expectsSuccess: false + expectedMessages: + - Expected literal of type date. + - rql: SEARCH RULES FOR "Test Content" SINCE 16.8 + expectsSuccess: false + expectedMessages: + - Expected literal of type date. + - rql: SEARCH RULES FOR "Test Content" SINCE false + expectsSuccess: false + expectedMessages: + - Expected literal of type date. + - rql: SEARCH RULES FOR "Test Content" SINCE nothing + expectsSuccess: false + expectedMessages: + - Expected literal of type date. + - rql: SEARCH RULES FOR 123 SINCE $2020-01-01$ UNTIL $2021-01-01$; + expectsSuccess: true + expectedMessages: [] + - rql: SEARCH RULES FOR true SINCE $2020-01-01$ UNTIL $2021-01-01$; + expectsSuccess: false + expectedMessages: + - Literal 'TRUE' is not allowed as a valid content type. Only literals of types [Integer, String] are allowed. + - rql: SEARCH RULES FOR 13.1 SINCE $2020-01-01$ UNTIL $2021-01-01$; + expectsSuccess: false + expectedMessages: + - Literal '13.1' is not allowed as a valid content type. Only literals of types [Integer, String] are allowed. + - rql: SEARCH RULES FOR NOTHING SINCE $2020-01-01$ UNTIL $2021-01-01$; + expectsSuccess: false + expectedMessages: + - Literal 'NOTHING' is not allowed as a valid content type. Only literals of types [Integer, String] are allowed. + - rql: SEARCH RULES FOR "Test Content" SINCE $2020-01-01$ UNTIL $2021-01-01$ WHEN { @SomeCondition is true }; + expectsSuccess: true + expectedMessages: [] + - rql: SEARCH RULES FOR "Test Content" SINCE $2020-01-01$ UNTIL $2021-01-01$ WHEN { @SomeCondition is true } + expectsSuccess: false + expectedMessages: + - Expected token ';'. + - rql: SEARCH RULES FOR "Test Content" SINCE $2020-01-01$ UNTIL $2021-01-01$ WHEN; + expectsSuccess: false + expectedMessages: + - Expected '{' after WITH. + - rql: SEARCH RULES FOR "Test Content" SINCE $2020-01-01$ UNTIL $2021-01-01$ WHEN {}; + expectsSuccess: false + expectedMessages: + - Expected placeholder (@) for condition. + - rql: SEARCH RULES FOR "Test Content" SINCE $2020-01-01$ UNTIL $2021-01-01$ WHEN { @SomeCondition }; + expectsSuccess: false + expectedMessages: + - Expected token 'IS'. + - rql: SEARCH RULES FOR "Test Content" SINCE $2020-01-01$ UNTIL $2021-01-01$ WHEN { @SomeCondition is }; + expectsSuccess: false + expectedMessages: + - Expected literal for condition. + - rql: SEARCH RULES FOR "Test Content" SINCE $2020-01-01$ UNTIL $2021-01-01$ WHEN { @SomeCondition is $2024-01-01$ }; + expectsSuccess: false + expectedMessages: + - Expected literal for condition. + - rql: SEARCH RULES FOR "Test Content" SINCE $2020-01-01$ UNTIL $2021-01-01$ WHEN { @SomeCondition is nothing }; + expectsSuccess: false + expectedMessages: + - Expected literal for condition. + - rql: SEARCH RULES FOR "Test Content" SINCE $2020-01-01$ UNTIL $2021-01-01$ WHEN { @SomeCondition is true, }; + expectsSuccess: false + expectedMessages: + - Expected placeholder (@) for condition. + - rql: SEARCH RULES FOR "Test Content" SINCE $2020-01-01$ UNTIL $2021-01-01$ WHEN { @SomeCondition is true + expectsSuccess: false + expectedMessages: + - Expected ',' or '}' after input condition. + - rql: SEARCH RULES FOR "Test Content" SINCE $2020-01-01$ UNTIL $2021-01-01$ WHEN { @SomeCondition is true, @OtherCondition is 123, @AnotherCondition is 123.45, @YetAnotherCondition is "Test" }; + expectsSuccess: true + expectedMessages: [] + - rql: SEARCH RULES FOR "Test Content" SINCE $2020-01-01$ UNTIL $2021-01-01$ test; + expectsSuccess: false + expectedMessages: + - Unrecognized token 'test'. \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/GrammarCheckLine.cs b/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/GrammarCheckLine.cs new file mode 100644 index 00000000..1c9ea833 --- /dev/null +++ b/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/GrammarCheckLine.cs @@ -0,0 +1,10 @@ +namespace Rules.Framework.Rql.IntegrationTests.GrammarCheck +{ + internal class GrammarCheckLine + { + public string[] ExpectedMessages { get; init; } + public bool ExpectsSuccess { get; init; } + public string Rql { get; init; } + public string[] Tags { get; init; } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/GrammarCheckTests.cs b/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/GrammarCheckTests.cs new file mode 100644 index 00000000..b30ab6b3 --- /dev/null +++ b/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/GrammarCheckTests.cs @@ -0,0 +1,132 @@ +namespace Rules.Framework.Rql.IntegrationTests.GrammarCheck +{ + using System.Reflection; + using System.Text; + using FluentAssertions; + using Rules.Framework.Rql.Pipeline.Parse; + using Rules.Framework.Rql.Pipeline.Scan; + using Xunit; + using Xunit.Abstractions; + using YamlDotNet.Serialization; + using YamlDotNet.Serialization.NamingConventions; + + public class GrammarCheckTests + { + private static readonly string[] checksFiles = + [ + "Rules.Framework.Rql.IntegrationTests.GrammarCheck.CheckFiles.MatchExpressionChecks.yaml", + "Rules.Framework.Rql.IntegrationTests.GrammarCheck.CheckFiles.BasicLanguageChecks.yaml", + "Rules.Framework.Rql.IntegrationTests.GrammarCheck.CheckFiles.SearchExpressionChecks.yaml", + ]; + + private readonly IParser parser; + private readonly IScanner scanner; + private readonly ITestOutputHelper testOutputHelper; + + public GrammarCheckTests(ITestOutputHelper testOutputHelper) + { + this.scanner = new Scanner(); + this.parser = new Parser(new ParseStrategyPool()); + this.testOutputHelper = testOutputHelper; + } + + public static IEnumerable GetTestCases() + { + foreach (var checksFile in checksFiles) + { + var checksStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(checksFile); + using (var checksStreamReader = new StreamReader(checksStream!)) + { + var deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + var checks = deserializer.Deserialize(checksStreamReader); + + foreach (var checkLine in checks.Checks) + { + yield return new object[] { checkLine.Rql, checkLine.ExpectsSuccess, checkLine.ExpectedMessages }; + } + } + } + + yield break; + } + + [Theory] + [MemberData(nameof(GetTestCases))] + public void CheckRqlGrammar(string rqlSource, bool expectsSuccess, IEnumerable expectedMessages) + { + // Arrange + var testOutputMessage = new StringBuilder() + .Append("RQL: ") + .AppendLine(rqlSource) + .Append("Is success expected? -> ") + .Append(expectsSuccess); + + if (expectedMessages.Any()) + { + testOutputMessage.AppendLine() + .AppendLine("Expected messages:"); + foreach (var message in expectedMessages) + { + testOutputMessage.Append(" - ") + .AppendLine(message); + } + } + + this.testOutputHelper.WriteLine(testOutputMessage.ToString()); + + // Act + var isSuccess = this.TryScanAndParse(rqlSource, out var actualMessages); + + testOutputMessage.Clear() + .Append("Outcome: ") + .Append(isSuccess); + + if (actualMessages.Any()) + { + testOutputMessage.AppendLine() + .AppendLine("Actual messages:"); + foreach (var message in actualMessages) + { + testOutputMessage.Append(" - ") + .AppendLine(message); + } + } + + this.testOutputHelper.WriteLine(testOutputMessage.ToString()); + + // Assert + isSuccess.Should().Be(expectsSuccess); + if (expectedMessages.Any()) + { + actualMessages.Should().Contain(expectedMessages); + } + else + { + actualMessages.Should().BeEmpty(); + } + } + + private bool TryScanAndParse(string rqlSource, out IEnumerable errorMessages) + { + var scanResult = this.scanner.ScanTokens(rqlSource); + if (!scanResult.Success) + { + errorMessages = scanResult.Messages.Select(x => x.Text).ToArray(); + return false; + } + + var parseResult = this.parser.Parse(scanResult.Tokens); + if (!parseResult.Success) + { + errorMessages = parseResult.Messages.Select(x => x.Text).ToArray(); + return false; + } + + errorMessages = Array.Empty(); + return true; + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/GrammarChecks.cs b/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/GrammarChecks.cs new file mode 100644 index 00000000..be388d81 --- /dev/null +++ b/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/GrammarChecks.cs @@ -0,0 +1,13 @@ +namespace Rules.Framework.Rql.IntegrationTests.GrammarCheck +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + + internal class GrammarChecks + { + public GrammarCheckLine[] Checks { get; set; } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.IntegrationTests/Rules.Framework.Rql.IntegrationTests.csproj b/tests/Rules.Framework.Rql.IntegrationTests/Rules.Framework.Rql.IntegrationTests.csproj new file mode 100644 index 00000000..69733d56 --- /dev/null +++ b/tests/Rules.Framework.Rql.IntegrationTests/Rules.Framework.Rql.IntegrationTests.csproj @@ -0,0 +1,46 @@ + + + + net8.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + + + + diff --git a/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/RqlMatchAllTestCase.cs b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/RqlMatchAllTestCase.cs new file mode 100644 index 00000000..8e81f526 --- /dev/null +++ b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/RqlMatchAllTestCase.cs @@ -0,0 +1,9 @@ +namespace Rules.Framework.Rql.IntegrationTests.Scenarios +{ + internal class RqlMatchAllTestCase + { + public bool ExpectsRules { get; set; } + public string? Rql { get; set; } + public string[]? RuleNames { get; set; } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/RqlMatchOneTestCase.cs b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/RqlMatchOneTestCase.cs new file mode 100644 index 00000000..95829cd7 --- /dev/null +++ b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/RqlMatchOneTestCase.cs @@ -0,0 +1,9 @@ +namespace Rules.Framework.Rql.IntegrationTests.Scenarios +{ + internal class RqlMatchOneTestCase + { + public bool ExpectsRule { get; set; } + public string? Rql { get; set; } + public string? RuleName { get; set; } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/RqlScenarioTestCases.cs b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/RqlScenarioTestCases.cs new file mode 100644 index 00000000..aa824d99 --- /dev/null +++ b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/RqlScenarioTestCases.cs @@ -0,0 +1,13 @@ +namespace Rules.Framework.Rql.IntegrationTests.Scenarios +{ + using System.Collections.Generic; + + internal class RqlScenarioTestCases + { + public IEnumerable MatchAllTestCases { get; set; } + + public IEnumerable MatchOneTestCases { get; set; } + + public IEnumerable SearchTestCases { get; set; } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/RqlSearchTestCase.cs b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/RqlSearchTestCase.cs new file mode 100644 index 00000000..160f1593 --- /dev/null +++ b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/RqlSearchTestCase.cs @@ -0,0 +1,9 @@ +namespace Rules.Framework.Rql.IntegrationTests.Scenarios +{ + internal class RqlSearchTestCase + { + public bool ExpectsRules { get; set; } + public string? Rql { get; set; } + public string[]? RuleNames { get; set; } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/RulesEngineWithScenario8RulesFixture.cs b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/RulesEngineWithScenario8RulesFixture.cs new file mode 100644 index 00000000..4a0d1a68 --- /dev/null +++ b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/RulesEngineWithScenario8RulesFixture.cs @@ -0,0 +1,38 @@ +namespace Rules.Framework.Rql.IntegrationTests.Scenarios.Scenario8 +{ + using System; + using Rules.Framework.IntegrationTests.Common.Scenarios; + using Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8; + + public class RulesEngineWithScenario8RulesFixture : IDisposable + { + public RulesEngineWithScenario8RulesFixture() + { + this.RulesEngine = RulesEngineBuilder.CreateRulesEngine() + .WithContentType() + .WithConditionType() + .SetInMemoryDataSource() + .Configure(options => + { + options.EnableCompilation = true; + }) + .Build(); + + var scenarioData = new Scenario8Data(); + + ScenarioLoader.LoadScenarioAsync(this.RulesEngine, scenarioData).GetAwaiter().GetResult(); + } + + public IRulesEngine RulesEngine { get; private set; } + + public void Dispose() + { + if (this.RulesEngine != null) + { + this.RulesEngine = null!; + } + + GC.SuppressFinalize(this); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/Scenario8TestCasesLoaderFixture.cs b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/Scenario8TestCasesLoaderFixture.cs new file mode 100644 index 00000000..6a557d6e --- /dev/null +++ b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/Scenario8TestCasesLoaderFixture.cs @@ -0,0 +1,39 @@ +namespace Rules.Framework.Rql.IntegrationTests.Scenarios.Scenario8 +{ + using System.Collections.Generic; + using System.Reflection; + using YamlDotNet.Serialization; + using YamlDotNet.Serialization.NamingConventions; + + public static class Scenario8TestCasesLoaderFixture + { + private const string testCasesFile = "Rules.Framework.Rql.IntegrationTests.Scenarios.Scenario8.TestCases.yaml"; + + static Scenario8TestCasesLoaderFixture() + { + var scenarioTestCases = LoadScenarioTestCases(); + MatchAllTestCases = scenarioTestCases.MatchAllTestCases.Select(tc => new object[] { tc.Rql!, tc.ExpectsRules, tc.RuleNames! }).ToList(); + MatchOneTestCases = scenarioTestCases.MatchOneTestCases.Select(tc => new object[] { tc.Rql!, tc.ExpectsRule, tc.RuleName! }).ToList(); + SearchTestCases = scenarioTestCases.SearchTestCases.Select(tc => new object[] { tc.Rql!, tc.ExpectsRules, tc.RuleNames! }).ToList(); + } + + public static IEnumerable? MatchAllTestCases { get; private set; } + + public static IEnumerable? MatchOneTestCases { get; private set; } + + public static IEnumerable? SearchTestCases { get; private set; } + + private static RqlScenarioTestCases LoadScenarioTestCases() + { + var testCasesStream = Assembly.GetExecutingAssembly().GetManifestResourceStream(testCasesFile); + using (var testCasesStreamReader = new StreamReader(testCasesStream!)) + { + var deserializer = new DeserializerBuilder() + .WithNamingConvention(CamelCaseNamingConvention.Instance) + .Build(); + + return deserializer.Deserialize(testCasesStreamReader); + } + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/TestCases.yaml b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/TestCases.yaml new file mode 100644 index 00000000..1035d3cc --- /dev/null +++ b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/TestCases.yaml @@ -0,0 +1,47 @@ +matchOneTestCases: + - rql: match one rule for "TexasHoldemPokerSingleCombinations" on $2023-01-01Z$ when { @NumberOfKings is 1, @NumberOfQueens is 1, @NumberOfJacks is 1, @NumberOfTens is 1, @NumberOfNines is 1, @KingOfClubs is true, @QueenOfDiamonds is true, @JackOfClubs is true, @TenOfHearts is true, @NineOfSpades is true }; + expectsRule: true + ruleName: Scenario 8 - Straight King, Queen, Jack, 10, 9 + - rql: match one rule for "TexasHoldemPokerSingleCombinations" on $2023-01-01Z$; + expectsRule: false + ruleName: + - rql: match one rule for "TexasHoldemPokerSingleCombinations" on $2023-05-03$ when { @NumberOfClubs is 6 }; + expectsRule: true + ruleName: Scenario 8 - Flush of Clubs + - rql: match one rule for "TexasHoldemPokerSingleCombinations" on $2023-05-03$ when { @NumberOfTreys is 3 }; + expectsRule: true + ruleName: Scenario 8 - Three Of A Kind Treys + - rql: match one rule for "TexasHoldemPokerSingleCombinations" on $2023-05-03$ when { @NumberOfDeuces is 2, @NumberOfFours is 2, @NumberOfJacks is 3 }; + expectsRule: true + ruleName: Scenario 8 - Three Of A Kind Jacks + - rql: match one rule for "TexasHoldemPokerSingleCombinations" on $2023-01-01Z$ when { @AceOfClubs is true, @KingOfClubs is true, @QueenOfClubs is true, @JackOfClubs is true, @TenOfClubs is true }; + expectsRule: true + ruleName: "Scenario 8 - Royal flush of Clubs: Ace, King, Queen, Jack, 10" +matchAllTestCases: + - rql: match all rules for "TexasHoldemPokerSingleCombinations" on $2023-01-01Z$ when { @NumberOfKings is 1, @NumberOfQueens is 1, @NumberOfJacks is 1, @NumberOfTens is 1, @NumberOfNines is 1, @KingOfClubs is true, @QueenOfDiamonds is true, @JackOfClubs is true, @TenOfHearts is true, @NineOfSpades is true }; + expectsRules: true + ruleNames: + - Scenario 8 - Straight King, Queen, Jack, 10, 9 + - Scenario 8 - High Card Kings + - Scenario 8 - High Card Queens + - Scenario 8 - High Card Jacks + - Scenario 8 - High Card Tens + - Scenario 8 - High Card Nines + - rql: match all rules for "TexasHoldemPokerSingleCombinations" on $2023-01-01Z$; + expectsRules: false + ruleNames: + - rql: match all rules for "TexasHoldemPokerSingleCombinations" on $2023-05-03$ when { @NumberOfDeuces is 2, @NumberOfFours is 2, @NumberOfJacks is 3 }; + expectsRules: true + ruleNames: + - Scenario 8 - Three Of A Kind Jacks + - Scenario 8 - Pair Fours + - Scenario 8 - Pair Deuces +searchTestCases: + - rql: search rules for "TexasHoldemPokerSingleCombinations" since $2023-01-01Z$ until $2023-01-31Z$ when { @NumberOfKings is 3 }; + expectsRules: true + ruleNames: + - Scenario 8 - Straight King, Queen, Jack, 10, 9 + - Scenario 8 - Three Of A Kind Kings + - rql: search rules for "TexasHoldemPokerSingleCombinations" since $2023-01-01Z$ until $2023-01-31Z$ when { @NumberOfAces is 5 }; + expectsRules: false + ruleNames: \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/TexasHoldEmPokerSingleCombinationsTests.cs b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/TexasHoldEmPokerSingleCombinationsTests.cs new file mode 100644 index 00000000..5a50e6bb --- /dev/null +++ b/tests/Rules.Framework.Rql.IntegrationTests/Scenarios/Scenario8/TexasHoldEmPokerSingleCombinationsTests.cs @@ -0,0 +1,139 @@ +namespace Rules.Framework.Rql.IntegrationTests.Scenarios.Scenario8 +{ + using System.Threading.Tasks; + using FluentAssertions; + using Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8; + using Rules.Framework.Rql.Runtime.Types; + using Xunit; + + public class TexasHoldEmPokerSingleCombinationsTests : IClassFixture + { + private readonly RulesEngineWithScenario8RulesFixture rulesEngineFixture; + + public TexasHoldEmPokerSingleCombinationsTests( + RulesEngineWithScenario8RulesFixture rulesEngineFixture) + { + this.rulesEngineFixture = rulesEngineFixture; + } + + [Theory] + [MemberData(nameof(Scenario8TestCasesLoaderFixture.MatchAllTestCases), MemberType = typeof(Scenario8TestCasesLoaderFixture))] + public async Task PokerCombinations_GivenMatchAllRqlStatement_EvaluatesAndReturnsResult(string rql, bool expectsRules, string[] ruleNames) + { + // Arrange + var rqlEngine = this.rulesEngineFixture.RulesEngine.GetRqlEngine(); + + // Act + var results = await rqlEngine.ExecuteAsync(rql); + + // Assert + results.Should().NotBeNull() + .And.HaveCount(1); + + var result = results.First(); + result.Should().NotBeNull(); + + if (expectsRules) + { + result.Should().BeOfType>(); + var rulesSetResult = (RulesSetResult)result; + rulesSetResult.NumberOfRules.Should().Be(ruleNames.Length); + rulesSetResult.Lines.Should().HaveCount(ruleNames.Length); + + for (int i = 0; i < ruleNames.Length; i++) + { + var rule = rulesSetResult.Lines[i].Rule.Value; + rule.Name.Should().Be(ruleNames[i]); + } + } + else + { + result.Should().BeOfType(); + var valueResult = (ValueResult)result; + valueResult.Value.Should().NotBeNull() + .And.BeOfType() + .And.Subject.As() + .Size.Value.Should().Be(0); + } + } + + [Theory] + [MemberData(nameof(Scenario8TestCasesLoaderFixture.MatchOneTestCases), MemberType = typeof(Scenario8TestCasesLoaderFixture))] + public async Task PokerCombinations_GivenMatchOneRqlStatement_EvaluatesAndReturnsResult(string rql, bool expectsRule, string ruleName) + { + // Arrange + var rqlEngine = this.rulesEngineFixture.RulesEngine.GetRqlEngine(); + + // Act + var results = await rqlEngine.ExecuteAsync(rql); + + // Assert + results.Should().NotBeNull() + .And.HaveCount(1); + + var result = results.First(); + result.Should().NotBeNull(); + + if (expectsRule) + { + result.Should().BeOfType>(); + var rulesSetResult = (RulesSetResult)result; + rulesSetResult.NumberOfRules.Should().Be(1); + rulesSetResult.Lines.Should().HaveCount(1); + + var rule = rulesSetResult.Lines[0].Rule.Value; + rule.Name.Should().Be(ruleName); + } + else + { + result.Should().BeOfType(); + var valueResult = (ValueResult)result; + valueResult.Value.Should().NotBeNull() + .And.BeOfType() + .And.Subject.As() + .Size.Value.Should().Be(0); + } + } + + [Theory] + [MemberData(nameof(Scenario8TestCasesLoaderFixture.SearchTestCases), MemberType = typeof(Scenario8TestCasesLoaderFixture))] + public async Task PokerCombinations_GivenSearchRqlStatement_EvaluatesAndReturnsResult(string rql, bool expectsRules, string[] ruleNames) + { + // Arrange + var rqlEngine = this.rulesEngineFixture.RulesEngine.GetRqlEngine(); + + // Act + var results = await rqlEngine.ExecuteAsync(rql); + + // Assert + results.Should().NotBeNull() + .And.HaveCount(1); + + var result = results.First(); + result.Should().NotBeNull(); + + if (expectsRules) + { + result.Should().BeOfType>(); + var rulesSetResult = (RulesSetResult)result; + rulesSetResult.NumberOfRules.Should().Be(ruleNames.Length); + rulesSetResult.Lines.Should().HaveCount(ruleNames.Length); + + for (int i = 0; i < ruleNames.Length; i++) + { + var rule = rulesSetResult.Lines[i].Rule.Value; + rule.Name.Should().Be(ruleNames[i]); + } + } + else + { + result.Should().BeOfType(); + var valueResult = (ValueResult)result; + valueResult.Value.Should().NotBeNull() + .And.BeOfType() + .And.Subject.As() + .Size.Value.Should().Be(0); + } + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/AssemblyMetadata.cs b/tests/Rules.Framework.Rql.Tests/AssemblyMetadata.cs new file mode 100644 index 00000000..79c09eb0 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/AssemblyMetadata.cs @@ -0,0 +1,7 @@ +using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; +using Xunit; + +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] +[assembly: ExcludeFromCodeCoverage] +[assembly: AssemblyTrait("Category", "Unit")] \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.BinaryExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.BinaryExpression.cs new file mode 100644 index 00000000..3c666a7e --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.BinaryExpression.cs @@ -0,0 +1,67 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Interpret +{ + using System.Threading.Tasks; + using FluentAssertions; + using Moq; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Tests.Stubs; + using Xunit; + + public partial class InterpreterTests + { + [Fact] + public async Task VisitBinaryExpression_GivenValidBinaryExpression_ProcessesRuleBinary() + { + // Arrange + var expected = NewRqlBool(false); + var leftExpression = CreateMockedExpression(NewRqlString("message")); + var operatorSegment = CreateMockedSegment(RqlOperators.Equals); + var rightExpression = CreateMockedExpression(NewRqlString("Hello world")); + var binaryExpression = new BinaryExpression(leftExpression, operatorSegment, rightExpression); + + var runtime = Mock.Of>(); + Mock.Get(runtime) + .Setup(r => r.ApplyBinary(It.IsAny(), It.IsAny(), It.IsAny())) + .Returns(expected); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitBinaryExpression(binaryExpression); + + // Assert + actual.Should().Be(expected); + Mock.Get(runtime) + .Verify(r => r.ApplyBinary(It.IsAny(), It.IsAny(), It.IsAny()), Times.Once()); + } + + [Fact] + public async Task VisitBinaryExpression_GivenValidBinaryExpressionFailingBinaryOnRuntime_ThrowsInterpreterExceptionWithErrorMessageFromRuntime() + { + // Arrange + var leftExpression = CreateMockedExpression(NewRqlString("message")); + var operatorSegment = CreateMockedSegment(RqlOperators.Equals); + var rightExpression = CreateMockedExpression(NewRqlString("Hello world")); + var binaryExpression = new BinaryExpression(leftExpression, operatorSegment, rightExpression); + + var runtime = Mock.Of>(); + const string expected = "An error has occurred"; + Mock.Get(runtime) + .Setup(r => r.ApplyBinary(It.IsAny(), It.IsAny(), It.IsAny())) + .Throws(new RuntimeException(expected)); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var interpreterException = await Assert.ThrowsAsync(async () => await interpreter.VisitBinaryExpression(binaryExpression)); + + // Assert + interpreterException.Should().NotBeNull(); + interpreterException.Message.Should().Contain(expected); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.CardinalitySegment.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.CardinalitySegment.cs new file mode 100644 index 00000000..ef287f8b --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.CardinalitySegment.cs @@ -0,0 +1,36 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Interpret +{ + using System.Threading.Tasks; + using FluentAssertions; + using Moq; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Ast.Segments; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Tests.Stubs; + using Xunit; + + public partial class InterpreterTests + { + [Fact] + public async Task VisitCardinalitySegment_GivenValidCardinalitySegment_ReturnsCardinalityValue() + { + // Arrange + var expected = NewRqlString("ONE"); + var cardinalityExpression = CreateMockedExpression(expected); + var ruleExpression = CreateMockedExpression(NewRqlString("rule")); + var cardinalitySegment = CardinalitySegment.Create(cardinalityExpression, ruleExpression); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitCardinalitySegment(cardinalitySegment); + + // Assert + actual.Should().NotBeNull().And.BeEquivalentTo(expected); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.ExpressionStatement.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.ExpressionStatement.cs new file mode 100644 index 00000000..0a61e62e --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.ExpressionStatement.cs @@ -0,0 +1,43 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Interpret +{ + using System.Threading.Tasks; + using FluentAssertions; + using Moq; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Ast.Statements; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Tests.Stubs; + using Xunit; + + public partial class InterpreterTests + { + [Fact] + public async Task VisitExpressionStatemet_GivenValidExpressionStatement_ReturnsExpressionResultWithRql() + { + // Arrange + var expectedValue = NewRqlString("test"); + var expectedRql = "test rql"; + var expression = CreateMockedExpression(expectedValue); + var expressionStatement = ExpressionStatement.Create(expression); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + Mock.Get(reverseRqlBuilder) + .Setup(x => x.BuildRql(It.IsIn(expressionStatement))) + .Returns(expectedRql); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitExpressionStatement(expressionStatement); + + // Assert + actual.Should().NotBeNull().And.BeOfType(); + actual.Rql.Should().Be(expectedRql); + actual.Success.Should().BeTrue(); + var actualExpressionStatementResult = actual as ExpressionStatementResult; + actualExpressionStatementResult.Result.Should().BeEquivalentTo(expectedValue); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.IdentifierExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.IdentifierExpression.cs new file mode 100644 index 00000000..70362b20 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.IdentifierExpression.cs @@ -0,0 +1,36 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Interpret +{ + using System.Threading.Tasks; + using FluentAssertions; + using Moq; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Tokens; + using Rules.Framework.Rql.Tests.Stubs; + using Xunit; + + public partial class InterpreterTests + { + [Fact] + public async Task VisitIdentifierExpression_GivenValidIdentifierExpression_ReturnsIdentifierLexeme() + { + // Arrange + var expected = NewRqlString("test"); + var identifierToken = NewToken("test", null, TokenType.IDENTIFIER); + var identifierExpression = new IdentifierExpression(identifierToken); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitIdentifierExpression(identifierExpression); + + // Assert + actual.Should().BeEquivalentTo(expected); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.InputConditionSegment.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.InputConditionSegment.cs new file mode 100644 index 00000000..91d71b78 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.InputConditionSegment.cs @@ -0,0 +1,69 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Interpret +{ + using System.Threading.Tasks; + using FluentAssertions; + using Moq; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Ast.Segments; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Tests.Stubs; + using Xunit; + + public partial class InterpreterTests + { + [Fact] + public async Task VisitInputConditionSegment_GivenInvalidConditionType_ThrowsInterpreterException() + { + // Arrange + var expectedRql = "@Dummy is \"test\""; + var conditionValue = "test"; + var leftExpression = CreateMockedExpression(NewRqlString("Dummy")); + var operatorToken = NewToken("is", null, Framework.Rql.Tokens.TokenType.IS); + var rightExpression = CreateMockedExpression(NewRqlString(conditionValue)); + var inputConditionSegment = new InputConditionSegment(leftExpression, operatorToken, rightExpression); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + Mock.Get(reverseRqlBuilder) + .Setup(x => x.BuildRql(It.IsIn(inputConditionSegment))) + .Returns(expectedRql); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await Assert.ThrowsAsync(async () => await interpreter.VisitInputConditionSegment(inputConditionSegment)); + + // Assert + actual.Should().NotBeNull(); + actual.Message.Should().Contain("Condition type of name ' \"Dummy\"' was not found."); + actual.Rql.Should().Be(expectedRql); + } + + [Fact] + public async Task VisitInputConditionSegment_GivenValidInputConditionSegment_ReturnsCondition() + { + // Arrange + var expectedConditionType = ConditionType.IsoCountryCode; + var expectedConditionValue = "test"; + var leftExpression = CreateMockedExpression(NewRqlString("IsoCountryCode")); + var operatorToken = NewToken("is", null, Framework.Rql.Tokens.TokenType.IS); + var rightExpression = CreateMockedExpression(NewRqlString(expectedConditionValue)); + var inputConditionSegment = new InputConditionSegment(leftExpression, operatorToken, rightExpression); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitInputConditionSegment(inputConditionSegment); + + // Assert + actual.Should().NotBeNull().And.BeOfType>(); + var actualCondition = actual as Condition; + actualCondition.Type.Should().Be(expectedConditionType); + actualCondition.Value.Should().Be(expectedConditionValue); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.InputConditionsSegment.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.InputConditionsSegment.cs new file mode 100644 index 00000000..1622d023 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.InputConditionsSegment.cs @@ -0,0 +1,40 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Interpret +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using FluentAssertions; + using Moq; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Ast.Segments; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Tests.Stubs; + using Xunit; + + public partial class InterpreterTests + { + [Fact] + public async Task VisitInputConditionsSegment_GivenValidInputConditionsSegment_ReturnsConditionsCollection() + { + // Arrange + var expectedCondition1 = new Condition(ConditionType.IsoCountryCode, "PT"); + var expectedCondition2 = new Condition(ConditionType.IsVip, true); + var inputConditionSegment1 = CreateMockedSegment(expectedCondition1); + var inputConditionSegment2 = CreateMockedSegment(expectedCondition2); + var inputConditionsSegment = new InputConditionsSegment(new[] { inputConditionSegment1, inputConditionSegment2 }); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitInputConditionsSegment(inputConditionsSegment); + + // Assert + actual.Should().NotBeNull().And.BeAssignableTo>>(); + var actualConditions = actual as IEnumerable>; + actualConditions.Should().ContainInOrder(expectedCondition1, expectedCondition2); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.KeywordExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.KeywordExpression.cs new file mode 100644 index 00000000..b7a5804f --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.KeywordExpression.cs @@ -0,0 +1,36 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Interpret +{ + using System.Threading.Tasks; + using FluentAssertions; + using Moq; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Tests.Stubs; + using Rules.Framework.Rql.Tokens; + using Xunit; + + public partial class InterpreterTests + { + [Fact] + public async Task VisitKeywordExpression_GivenValidKeywordExpression_ReturnsLexeme() + { + // Arrange + var expected = NewRqlString("var"); + var keywordToken = NewToken("var", null, TokenType.VAR); + var keywordExpression = KeywordExpression.Create(keywordToken); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitKeywordExpression(keywordExpression); + + // Assert + actual.Should().BeEquivalentTo(expected); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.LiteralExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.LiteralExpression.cs new file mode 100644 index 00000000..7fe1b144 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.LiteralExpression.cs @@ -0,0 +1,74 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Interpret +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using FluentAssertions; + using Moq; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Tests.Stubs; + using Rules.Framework.Rql.Tokens; + using Xunit; + + public partial class InterpreterTests + { + public static IEnumerable ValidCasesLiteralExpression => new[] + { + new object?[] { LiteralType.Bool, null, NewRqlNothing() }, + new object?[] { LiteralType.Bool, true, NewRqlBool(true) }, + new object?[] { LiteralType.Decimal, null, NewRqlNothing() }, + new object?[] { LiteralType.Decimal, 10.5m, NewRqlDecimal(10.5m) }, + new object?[] { LiteralType.Integer, null, NewRqlNothing() }, + new object?[] { LiteralType.Integer, 1, NewRqlInteger(1) }, + new object?[] { LiteralType.String, null, NewRqlNothing() }, + new object?[] { LiteralType.String, "test", NewRqlString("test") }, + new object?[] { LiteralType.DateTime, null, NewRqlNothing() }, + new object?[] { LiteralType.DateTime, new DateTime(2024, 1, 1), NewRqlDate(new DateTime(2024, 1, 1)) }, + new object?[] { LiteralType.Undefined, null, NewRqlNothing() }, + new object?[] { (LiteralType)(-1), null, NewRqlNothing() }, + }; + + [Fact] + public async Task VisitLiteralExpression_GivenLiteralExpressionWithUnsupportedLiteralType_ThrowsNotSupportedException() + { + // Arrange + var literalToken = NewToken("dummy", "dummy", TokenType.IDENTIFIER); + var literalExpression = LiteralExpression.Create((LiteralType)(-1), literalToken, "test"); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await Assert.ThrowsAsync(async () => await interpreter.VisitLiteralExpression(literalExpression)); + + // Assert + actual.Message.Should().Be("Literal with type '-1' is not supported."); + } + + [Theory] + [MemberData(nameof(ValidCasesLiteralExpression))] + public async Task VisitLiteralExpression_GivenValidLiteralExpression_ReturnsRuntimeValue(object literalType, object? runtimeValue, object expected) + { + // Arrange + var literalToken = NewToken("dummy", expected, TokenType.IDENTIFIER); + var literalExpression = LiteralExpression.Create((LiteralType)literalType, literalToken, runtimeValue); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitLiteralExpression(literalExpression); + + // Assert + actual.Should().NotBeNull().And.BeEquivalentTo(expected); + actual.RuntimeValue.Should().Be(runtimeValue); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.MatchExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.MatchExpression.cs new file mode 100644 index 00000000..f5abe0a3 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.MatchExpression.cs @@ -0,0 +1,146 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Interpret +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using FluentAssertions; + using Moq; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Runtime.Types; + using Rules.Framework.Rql.Tests.Stubs; + using Xunit; + + public partial class InterpreterTests + { + public static IEnumerable ValidCasesMatchExpression => new[] + { + new object[] { "one", NewRqlString("Type1"), true }, + new object[] { "one", NewRqlAny(NewRqlString("Type1")), true }, + new object[] { "one", NewRqlString("Type1"), false }, + new object[] { "one", NewRqlAny(NewRqlString("Type1")), false }, + new object[] { "all", NewRqlString("Type1"), true }, + new object[] { "all", NewRqlAny(NewRqlString("Type1")), true }, + new object[] { "all", NewRqlString("Type1"), false }, + new object[] { "one", NewRqlAny(NewRqlString("Type1")), false }, + }; + + [Fact] + public async Task VisitMatchExpression_GivenInvalidMatchExpressionWithInvalidContentType_ThrowsInterpreterException() + { + // Arrange + var conditions = new[] { new Condition(ConditionType.IsVip, false) }; + + var cardinalitySegment = CreateMockedSegment(NewRqlString("one")); + var contentTypeExpression = CreateMockedExpression(NewRqlDecimal(1m)); + var matchDateExpression = CreateMockedExpression(NewRqlDate(new DateTime(2024, 1, 1))); + var inputConditionsSegment = CreateMockedSegment(conditions); + var matchExpression = MatchExpression.Create(cardinalitySegment, contentTypeExpression, matchDateExpression, inputConditionsSegment); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await Assert.ThrowsAsync(async () => await interpreter.VisitMatchExpression(matchExpression)); + + // Act + actual.Message.Should().Contain("Expected a content type value of type 'string' but found 'decimal' instead"); + } + + [Fact] + public async Task VisitMatchExpression_GivenInvalidMatchExpressionWithUnknownContentType_ThrowsInterpreterException() + { + // Arrange + var conditions = new[] { new Condition(ConditionType.IsVip, false) }; + + var cardinalitySegment = CreateMockedSegment(NewRqlString("one")); + var contentTypeExpression = CreateMockedExpression(NewRqlString("dummy")); + var matchDateExpression = CreateMockedExpression(NewRqlDate(new DateTime(2024, 1, 1))); + var inputConditionsSegment = CreateMockedSegment(conditions); + var matchExpression = MatchExpression.Create(cardinalitySegment, contentTypeExpression, matchDateExpression, inputConditionsSegment); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await Assert.ThrowsAsync(async () => await interpreter.VisitMatchExpression(matchExpression)); + + // Act + actual.Message.Should().Contain("The content type value 'dummy' was not found"); + } + + [Fact] + public async Task VisitMatchExpression_GivenMatchExpressionFailingRuntimeEvaluation_ThrowsInterpreterException() + { + // Arrange + var conditions = new[] { new Condition(ConditionType.IsVip, false) }; + + var cardinalitySegment = CreateMockedSegment(NewRqlString("one")); + var contentTypeExpression = CreateMockedExpression(NewRqlString("Type1")); + var matchDateExpression = CreateMockedExpression(NewRqlDate(new DateTime(2024, 1, 1))); + var inputConditionsSegment = CreateMockedSegment(conditions); + var matchExpression = MatchExpression.Create(cardinalitySegment, contentTypeExpression, matchDateExpression, inputConditionsSegment); + + var runtime = Mock.Of>(); + Mock.Get(runtime) + .Setup(x => x.MatchRulesAsync(It.IsAny>())) + .Throws(new RuntimeException("test")); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await Assert.ThrowsAsync(async () => await interpreter.VisitMatchExpression(matchExpression)); + + // Act + actual.Message.Should().Contain("test"); + } + + [Theory] + [MemberData(nameof(ValidCasesMatchExpression))] + public async Task VisitMatchExpression_GivenValidMatchExpressionForOneCardinality_ReturnsOneRule( + string cardinalityName, + object contentTypeName, + bool hasConditions) + { + // Arrange + var ruleResult = RuleBuilder.NewRule() + .WithName("Dummy rule") + .WithDateBegin(DateTime.Now) + .WithContent(ContentType.Type1, "test") + .WithCondition(x => x.Value(ConditionType.IsVip, Framework.Core.Operators.Equal, false)) + .Build(); + var conditions = hasConditions ? new[] { new Condition(ConditionType.IsVip, false) } : null; + + var expected = new RqlArray(1); + expected.SetAtIndex(0, new RqlRule(ruleResult.Rule)); + + var cardinalitySegment = CreateMockedSegment(NewRqlString(cardinalityName)); + var contentTypeExpression = CreateMockedExpression((IRuntimeValue)contentTypeName); + var matchDateExpression = CreateMockedExpression(NewRqlDate(new DateTime(2024, 1, 1))); + var inputConditionsSegment = CreateMockedSegment(conditions); + var matchExpression = MatchExpression.Create(cardinalitySegment, contentTypeExpression, matchDateExpression, inputConditionsSegment); + + var runtime = Mock.Of>(); + Mock.Get(runtime) + .Setup(x => x.MatchRulesAsync(It.IsAny>())) + .Returns(new ValueTask(expected)); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitMatchExpression(matchExpression); + + // Act + actual.Should().NotBeNull() + .And.BeEquivalentTo(expected); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.NewArrayExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.NewArrayExpression.cs new file mode 100644 index 00000000..f7155384 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.NewArrayExpression.cs @@ -0,0 +1,81 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Interpret +{ + using System; + using System.Threading.Tasks; + using FluentAssertions; + using Moq; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Runtime.Types; + using Rules.Framework.Rql.Tests.Stubs; + using Xunit; + + public partial class InterpreterTests + { + [Fact] + public async Task VisitNewArrayExpression_GivenValidNewArrayExpressionWithSizeInitializer_ReturnsArrayFilledWithRqlNothing() + { + // Arrange + var arrayToken = NewToken("array", null, Framework.Rql.Tokens.TokenType.ARRAY); + var initializerBeginToken = NewToken("[", null, Framework.Rql.Tokens.TokenType.STRAIGHT_BRACKET_LEFT); + var sizeExpression = CreateMockedExpression(NewRqlInteger(2)); + var values = Array.Empty(); + var initializerEndToken = NewToken("]", null, Framework.Rql.Tokens.TokenType.STRAIGHT_BRACKET_RIGHT); + + var newArrayExpression = NewArrayExpression.Create(arrayToken, initializerBeginToken, sizeExpression, values, initializerEndToken); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitNewArrayExpression(newArrayExpression); + + // Assert + actual.Should().NotBeNull() + .And.BeOfType(); + var array = (RqlArray)actual; + array.Size.Should().Be(NewRqlInteger(2)); + array.Value.Should().AllSatisfy(i => i.Unwrap().Should().BeOfType()); + } + + [Fact] + public async Task VisitNewArrayExpression_GivenValidNewArrayExpressionWithValuesInitializer_ReturnsArrayFilledWithValues() + { + // Arrange + var arrayToken = NewToken("array", null, Framework.Rql.Tokens.TokenType.ARRAY); + var initializerBeginToken = NewToken("{", null, Framework.Rql.Tokens.TokenType.BRACE_LEFT); + var sizeExpression = CreateMockedExpression(NewRqlNothing()); + var values = new[] + { + CreateMockedExpression(NewRqlInteger(1)), + CreateMockedExpression(NewRqlString("test")), + CreateMockedExpression(NewRqlBool(true)), + }; + var initializerEndToken = NewToken("}", null, Framework.Rql.Tokens.TokenType.BRACE_RIGHT); + + var newArrayExpression = NewArrayExpression.Create(arrayToken, initializerBeginToken, sizeExpression, values, initializerEndToken); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitNewArrayExpression(newArrayExpression); + + // Assert + actual.Should().NotBeNull() + .And.BeOfType(); + var array = (RqlArray)actual; + array.Size.Should().Be(NewRqlInteger(3)); + array.Value.Should().SatisfyRespectively( + v => v.Unwrap().Value.Should().Be(1), + v => v.Unwrap().Value.Should().Be("test"), + v => v.Unwrap().Value.Should().BeTrue()); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.NewObjectExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.NewObjectExpression.cs new file mode 100644 index 00000000..ea72752f --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.NewObjectExpression.cs @@ -0,0 +1,54 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Interpret +{ + using System.Collections.Generic; + using System.Threading.Tasks; + using FluentAssertions; + using Moq; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Runtime.Types; + using Rules.Framework.Rql.Tests.Stubs; + using Xunit; + + public partial class InterpreterTests + { + [Fact] + public async Task VisitNewObjectExpression_GivenValidNewObjectExpressionWithPropertiesInitializer_ReturnsObjectWithPropertiesFilled() + { + // Arrange + var objectToken = NewToken("object", null, Framework.Rql.Tokens.TokenType.OBJECT); + var assignementToken = NewToken("=", null, Framework.Rql.Tokens.TokenType.ASSIGN); + var values = new[] + { + new AssignmentExpression( + CreateMockedExpression(NewRqlString("Name")), + assignementToken, + CreateMockedExpression(NewRqlString("Roger"))), + new AssignmentExpression( + CreateMockedExpression(NewRqlString("Age")), + assignementToken, + CreateMockedExpression(NewRqlInteger(25))), + }; + + var newObjectExpression = new NewObjectExpression(objectToken, values); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitNewObjectExpression(newObjectExpression); + + // Assert + actual.Should().NotBeNull() + .And.BeOfType(); + var objProperties = (IDictionary)actual.RuntimeValue; + objProperties.Should().NotBeNullOrEmpty() + .And.Contain("Name", "Roger") + .And.Contain("Age", 25); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.None.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.None.cs new file mode 100644 index 00000000..3fbf0e35 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.None.cs @@ -0,0 +1,77 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Interpret +{ + using System.Threading.Tasks; + using FluentAssertions; + using Moq; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Ast.Segments; + using Rules.Framework.Rql.Ast.Statements; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Runtime.Types; + using Rules.Framework.Rql.Tests.Stubs; + using Xunit; + + public partial class InterpreterTests + { + [Fact] + public async Task VisitNoneExpression_GivenNoneExpression_ReturnsRqlNothing() + { + // Arrange + var noneExpression = new NoneExpression(); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitNoneExpression(noneExpression); + + // Assert + actual.Should().NotBeNull() + .And.BeOfType(); + } + + [Fact] + public async Task VisitNoneSegment_GivenNoneSegment_ReturnsNull() + { + // Arrange + var noneSegment = new NoneSegment(); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitNoneSegment(noneSegment); + + // Assert + actual.Should().BeNull(); + } + + [Fact] + public async Task VisitNoneStatement_GivenNoneStatement_ReturnsExpressionStatementWithRqlNothing() + { + // Arrange + var noneStatement = new NoneStatement(); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitNoneStatement(noneStatement); + + // Assert + actual.Should().NotBeNull() + .And.BeOfType(); + var actualExpressionStatementResult = actual as ExpressionStatementResult; + actualExpressionStatementResult.Rql.Should().BeEmpty(); + actualExpressionStatementResult.Result.Should().BeOfType(); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.OperatorSegment.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.OperatorSegment.cs new file mode 100644 index 00000000..82ee92ac --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.OperatorSegment.cs @@ -0,0 +1,89 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Interpret +{ + using System; + using System.Linq; + using System.Threading.Tasks; + using FluentAssertions; + using Moq; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Ast.Segments; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Tokens; + using Rules.Framework.Rql.Tests.Stubs; + using Xunit; + + public partial class InterpreterTests + { + [Theory] + [InlineData(new object[] { TokenType.AND }, RqlOperators.And)] + [InlineData(new object[] { TokenType.ASSIGN }, RqlOperators.Assign)] + [InlineData(new object[] { TokenType.EQUAL }, RqlOperators.Equals)] + [InlineData(new object[] { TokenType.GREATER_THAN }, RqlOperators.GreaterThan)] + [InlineData(new object[] { TokenType.GREATER_THAN_OR_EQUAL }, RqlOperators.GreaterThanOrEquals)] + [InlineData(new object[] { TokenType.IN }, RqlOperators.In)] + [InlineData(new object[] { TokenType.LESS_THAN }, RqlOperators.LesserThan)] + [InlineData(new object[] { TokenType.LESS_THAN_OR_EQUAL }, RqlOperators.LesserThanOrEquals)] + [InlineData(new object[] { TokenType.MINUS }, RqlOperators.Minus)] + [InlineData(new object[] { TokenType.NOT, TokenType.IN }, RqlOperators.NotIn)] + [InlineData(new object[] { TokenType.NOT_EQUAL }, RqlOperators.NotEquals)] + [InlineData(new object[] { TokenType.OR }, RqlOperators.Or)] + [InlineData(new object[] { TokenType.PLUS }, RqlOperators.Plus)] + [InlineData(new object[] { TokenType.SLASH }, RqlOperators.Slash)] + [InlineData(new object[] { TokenType.STAR }, RqlOperators.Star)] + public async Task VisitOperatorSegment_GivenOperatorSegmentWithSupportedOperatorToken_ReturnsRqlOperator(object[] tokenTypes, object expected) + { + // Arrange + var operatorSegment = new OperatorSegment(tokenTypes.Select(tt => NewToken("test", null, (TokenType)tt)).ToArray()); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitOperatorSegment(operatorSegment); + + // Assert + actual.Should().Be(expected); + } + + [Fact] + public async Task VisitOperatorSegment_GivenOperatorSegmentWithUnsupportedOperatorToken_ThrowsNotSupportedException() + { + // Arrange + var operatorSegment = new OperatorSegment(new[] { NewToken("test", null, TokenType.NOT) }); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actualException = await Assert.ThrowsAsync(async () => await interpreter.VisitOperatorSegment(operatorSegment)); + + // Assert + actualException.Message.Should().Be($"The tokens with types ['NOT'] are not supported as a valid operator."); + } + + [Theory] + [InlineData(TokenType.ALL, TokenType.INT)] + [InlineData(TokenType.NOT, TokenType.INT)] + public async Task VisitOperatorSegment_GivenOperatorSegmentWithUnsupportedOperatorTokens_ThrowsNotSupportedException(object tokenType1, object tokenType2) + { + // Arrange + var operatorSegment = new OperatorSegment(new[] { NewToken("test", null, (TokenType)tokenType1), NewToken("test", null, (TokenType)tokenType2) }); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actualException = await Assert.ThrowsAsync(async () => await interpreter.VisitOperatorSegment(operatorSegment)); + + // Assert + actualException.Message.Should().Be($"The tokens with types ['{tokenType1}', '{tokenType2}'] are not supported as a valid operator."); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.PlaceholderExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.PlaceholderExpression.cs new file mode 100644 index 00000000..24b586b3 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.PlaceholderExpression.cs @@ -0,0 +1,40 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Interpret +{ + using System; + using System.Collections.Generic; + using System.Linq; + using System.Text; + using System.Threading.Tasks; + using FluentAssertions; + using Moq; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Runtime.Types; + using Rules.Framework.Rql.Tests.Stubs; + using Xunit; + + public partial class InterpreterTests + { + [Fact] + public async Task VisitPlaceholderExpression_GivenPlaceholderExpression_ReturnsRqlStringWithPlaceholderName() + { + // Arrange + var placeholderExpression = new PlaceholderExpression(NewToken("testPlaceholder", "testPlaceholder", Framework.Rql.Tokens.TokenType.PLACEHOLDER)); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitPlaceholderExpression(placeholderExpression); + + // Assert + actual.Should().BeOfType(); + var actualString = (RqlString)actual; + actualString.Value.Should().Be("testPlaceholder"); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.SearchExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.SearchExpression.cs new file mode 100644 index 00000000..e744a0ee --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.SearchExpression.cs @@ -0,0 +1,141 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Interpret +{ + using System; + using System.Collections.Generic; + using System.Threading.Tasks; + using FluentAssertions; + using Moq; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Runtime.Types; + using Rules.Framework.Rql.Tests.Stubs; + using Xunit; + + public partial class InterpreterTests + { + public static IEnumerable ValidCasesSearchExpression => new[] + { + new object[] { NewRqlString("Type1"), true }, + new object[] { NewRqlAny(NewRqlString("Type1")), true }, + new object[] { NewRqlString("Type1"), false }, + new object[] { NewRqlAny(NewRqlString("Type1")), false }, + }; + + [Fact] + public async Task VisitSearchExpression_GivenInvalidSearchExpressionWithInvalidContentType_ThrowsInterpreterException() + { + // Arrange + var conditions = new[] { new Condition(ConditionType.IsVip, false) }; + + var contentTypeExpression = CreateMockedExpression(NewRqlDecimal(1m)); + var dateBeginExpression = CreateMockedExpression(NewRqlDate(new DateTime(2024, 1, 1))); + var dateEndExpression = CreateMockedExpression(NewRqlDate(new DateTime(2024, 12, 31))); + var inputConditionsSegment = CreateMockedSegment(conditions); + var searchExpression = new SearchExpression(contentTypeExpression, dateBeginExpression, dateEndExpression, inputConditionsSegment); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await Assert.ThrowsAsync(async () => await interpreter.VisitSearchExpression(searchExpression)); + + // Act + actual.Message.Should().Contain("Expected a content type value of type 'string' but found 'decimal' instead"); + } + + [Fact] + public async Task VisitSearchExpression_GivenInvalidSearchExpressionWithUnknownContentType_ThrowsInterpreterException() + { + // Arrange + var conditions = new[] { new Condition(ConditionType.IsVip, false) }; + + var contentTypeExpression = CreateMockedExpression(NewRqlString("dummy")); + var dateBeginExpression = CreateMockedExpression(NewRqlDate(new DateTime(2024, 1, 1))); + var dateEndExpression = CreateMockedExpression(NewRqlDate(new DateTime(2024, 12, 31))); + var inputConditionsSegment = CreateMockedSegment(conditions); + var searchExpression = new SearchExpression(contentTypeExpression, dateBeginExpression, dateEndExpression, inputConditionsSegment); + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await Assert.ThrowsAsync(async () => await interpreter.VisitSearchExpression(searchExpression)); + + // Act + actual.Message.Should().Contain("The content type value 'dummy' was not found"); + } + + [Fact] + public async Task VisitSearchExpression_GivenSearchExpressionFailingRuntimeEvaluation_ThrowsInterpreterException() + { + // Arrange + var conditions = new[] { new Condition(ConditionType.IsVip, false) }; + + var contentTypeExpression = CreateMockedExpression(NewRqlString("Type1")); + var dateBeginExpression = CreateMockedExpression(NewRqlDate(new DateTime(2024, 1, 1))); + var dateEndExpression = CreateMockedExpression(NewRqlDate(new DateTime(2024, 12, 31))); + var inputConditionsSegment = CreateMockedSegment(conditions); + var searchExpression = new SearchExpression(contentTypeExpression, dateBeginExpression, dateEndExpression, inputConditionsSegment); + + var runtime = Mock.Of>(); + Mock.Get(runtime) + .Setup(x => x.SearchRulesAsync(It.IsAny>())) + .Throws(new RuntimeException("test")); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await Assert.ThrowsAsync(async () => await interpreter.VisitSearchExpression(searchExpression)); + + // Act + actual.Message.Should().Contain("test"); + } + + [Theory] + [MemberData(nameof(ValidCasesSearchExpression))] + public async Task VisitSearchExpression_GivenValidSearchExpressionForOneCardinality_ReturnsRqlArrayWithOneRule( + object contentTypeName, + bool hasConditions) + { + // Arrange + var ruleResult = RuleBuilder.NewRule() + .WithName("Dummy rule") + .WithDateBegin(DateTime.Now) + .WithContent(ContentType.Type1, "test") + .WithCondition(x => x.Value(ConditionType.IsVip, Framework.Core.Operators.Equal, false)) + .Build(); + var conditions = hasConditions ? new[] { new Condition(ConditionType.IsVip, false) } : null; + + var expected = new RqlArray(1); + expected.SetAtIndex(0, new RqlRule(ruleResult.Rule)); + + var contentTypeExpression = CreateMockedExpression((IRuntimeValue)contentTypeName); + var dateBeginExpression = CreateMockedExpression(NewRqlDate(new DateTime(2024, 1, 1))); + var dateEndExpression = CreateMockedExpression(NewRqlDate(new DateTime(2024, 12, 31))); + var inputConditionsSegment = CreateMockedSegment(conditions); + var searchExpression = new SearchExpression(contentTypeExpression, dateBeginExpression, dateEndExpression, inputConditionsSegment); + + var runtime = Mock.Of>(); + Mock.Get(runtime) + .Setup(x => x.SearchRulesAsync(It.IsAny>())) + .Returns(new ValueTask(expected)); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitSearchExpression(searchExpression); + + // Act + actual.Should().NotBeNull() + .And.BeEquivalentTo(expected); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.UnaryExpression.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.UnaryExpression.cs new file mode 100644 index 00000000..02b1e422 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.UnaryExpression.cs @@ -0,0 +1,63 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Interpret +{ + using System.Threading.Tasks; + using FluentAssertions; + using Moq; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Runtime.Types; + using Rules.Framework.Rql.Tests.Stubs; + using Xunit; + + public partial class InterpreterTests + { + [Fact] + public async Task VisitUnaryExpression_GivenUnaryExpressionWithKnownOperator_AppliesOperatorAndReturnsValue() + { + // Arrange + var minusToken = NewToken("-", null, Framework.Rql.Tokens.TokenType.MINUS); + var targetExpression = CreateMockedExpression(NewRqlInteger(10)); + var unaryExpression = new UnaryExpression(minusToken, targetExpression); + + var runtime = Mock.Of>(); + Mock.Get(runtime) + .Setup(x => x.ApplyUnary(new RqlInteger(10), RqlOperators.Minus)) + .Returns(new RqlInteger(-10)); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.VisitUnaryExpression(unaryExpression); + + // Assert + actual.Should().BeOfType() + .And.Subject.As().Value.Should().Be(-10); + } + + [Fact] + public async Task VisitUnaryExpression_GivenUnaryExpressionWithUnknownOperator_ThrowsInterpreterException() + { + // Arrange + var minusToken = NewToken("+", null, Framework.Rql.Tokens.TokenType.PLUS); + var targetExpression = CreateMockedExpression(NewRqlInteger(10)); + var unaryExpression = new UnaryExpression(minusToken, targetExpression); + + var runtime = Mock.Of>(); + Mock.Get(runtime) + .Setup(x => x.ApplyUnary(new RqlInteger(10), RqlOperators.None)) + .Throws(new RuntimeException("Unexpected operator")); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actualException = await Assert.ThrowsAsync(async () => await interpreter.VisitUnaryExpression(unaryExpression)); + + // Assert + actualException.Message.Should().Contain("Unexpected operator"); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.cs new file mode 100644 index 00000000..254737e2 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.cs @@ -0,0 +1,134 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Interpret +{ + using System; + using System.Threading.Tasks; + using FluentAssertions; + using Moq; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Ast.Segments; + using Rules.Framework.Rql.Ast.Statements; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Runtime.Types; + using Rules.Framework.Rql.Tests.Stubs; + using Rules.Framework.Rql.Tokens; + using Xunit; + + public partial class InterpreterTests + { + [Fact] + public async Task InterpretAsync_GivenInvalidStatementThatIssuesAnError_ReturnsErrorStatementResult() + { + // Arrange + var expectedException = new InterpreterException("abc", "rql", RqlSourcePosition.From(1, 1), RqlSourcePosition.From(1, 10)); + var expected = new ErrorStatementResult(expectedException.Message, expectedException.Rql, expectedException.BeginPosition, expectedException.EndPosition); + var mockStatementToExecute = new Mock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + mockStatementToExecute + .Setup(s => s.Accept(It.IsAny>>())) + .Throws(expectedException); + var statements = new[] { mockStatementToExecute.Object }; + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.InterpretAsync(statements); + + // Assert + actual.Should().NotBeNull() + .And.BeOfType(); + var interpretResult = actual as InterpretResult; + interpretResult.Results.Should().HaveCount(1) + .And.ContainEquivalentOf(expected); + } + + [Fact] + public async Task InterpretAsync_GivenValidStatement_ExecutesAndReturnsResult() + { + // Arrange + var expected = new NothingStatementResult("abc"); + var mockStatementToExecute = new Mock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + mockStatementToExecute + .Setup(s => s.Accept(It.IsAny>>())) + .Returns(Task.FromResult(expected)); + var statements = new[] { mockStatementToExecute.Object }; + + var runtime = Mock.Of>(); + var reverseRqlBuilder = Mock.Of(); + + var interpreter = new Interpreter(runtime, reverseRqlBuilder); + + // Act + var actual = await interpreter.InterpretAsync(statements); + + // Assert + actual.Should().NotBeNull() + .And.BeOfType(); + var interpretResult = actual as InterpretResult; + interpretResult.Results.Should().HaveCount(1) + .And.Contain(expected); + } + + private static Expression CreateMockedExpression(IRuntimeValue visitResult) + { + var mockExpression = new Mock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + mockExpression.Setup(e => e.Accept(It.IsAny>>())) + .Returns(Task.FromResult(visitResult)); + return mockExpression.Object; + } + + private static Segment CreateMockedSegment(object visitResult) + { + var mockSegment = new Mock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + mockSegment.Setup(e => e.Accept(It.IsAny>>())) + .Returns(Task.FromResult(visitResult)); + return mockSegment.Object; + } + + private static Statement CreateMockedStatement(Framework.Rql.Pipeline.Interpret.IResult visitResult) + { + var mockStatement = new Mock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + mockStatement.Setup(s => s.Accept(It.IsAny>>())) + .Returns(Task.FromResult(visitResult)); + return mockStatement.Object; + } + + private static RqlAny NewRqlAny(IRuntimeValue runtimeValue) + => new RqlAny(runtimeValue); + + private static RqlArray NewRqlArray(params IRuntimeValue[] runtimeValues) + { + var rqlArray = new RqlArray(runtimeValues.Length); + for (int i = 0; i < runtimeValues.Length; i++) + { + rqlArray.SetAtIndex(i, NewRqlAny(runtimeValues[i])); + } + + return rqlArray; + } + + private static RqlBool NewRqlBool(bool value) + => new RqlBool(value); + + private static RqlDate NewRqlDate(DateTime value) + => new RqlDate(value); + + private static RqlDecimal NewRqlDecimal(decimal value) + => new RqlDecimal(value); + + private static RqlInteger NewRqlInteger(int value) + => new RqlInteger(value); + + private static RqlNothing NewRqlNothing() + => new RqlNothing(); + + private static RqlString NewRqlString(string value) + => new RqlString(value); + + private static Token NewToken(string lexeme, object? value, TokenType type) + => Token.Create(lexeme, false, value, RqlSourcePosition.Empty, RqlSourcePosition.Empty, (uint)lexeme.Length, type); + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Parse/ParseContextTests.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Parse/ParseContextTests.cs new file mode 100644 index 00000000..f019a0f8 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Parse/ParseContextTests.cs @@ -0,0 +1,304 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Parse +{ + using System.Collections.Generic; + using FluentAssertions; + using Rules.Framework.Rql.Pipeline.Parse; + using Rules.Framework.Rql.Tokens; + using Xunit; + + public class ParseContextTests + { + private readonly IReadOnlyList _tokens; + + public ParseContextTests() + { + this._tokens = new List + { + Token.Create("test1", false, null, RqlSourcePosition.From(1, 1), RqlSourcePosition.From(1, 10), 10, TokenType.STRING), + Token.Create("test2", false, null, RqlSourcePosition.From(1, 11), RqlSourcePosition.From(1, 20), 10, TokenType.STRING), + Token.Create("test3", false, null, RqlSourcePosition.From(1, 21), RqlSourcePosition.From(1, 30), 10, TokenType.EOF), + }; + } + + [Fact] + public void EnterPanicMode_WhenInPanicModeAlready_ThrowsInvalidOperationException() + { + // Arrange + var parseContext = new ParseContext(this._tokens); + var token = this._tokens.First(); + parseContext.EnterPanicMode("Panic is installed", token); + + // Act + var exception = Assert.Throws(() => parseContext.EnterPanicMode("More panic", token)); + + // Assert + exception.Should().NotBeNull(); + exception.Message.Should().Be("Parse operation is already in panic mode."); + } + + [Fact] + public void EnterPanicMode_WhenNotInPanicModeAlready_SetsPanicModeInfo() + { + // Arrange + var parseContext = new ParseContext(this._tokens); + var token = this._tokens.First(); + + // Act + parseContext.EnterPanicMode("Panic is installed", token); + + // Assert + parseContext.PanicMode.Should().BeTrue(); + parseContext.PanicModeInfo.Should().NotBeNull(); + parseContext.PanicModeInfo.Message.Should().Be("Panic is installed"); + parseContext.PanicModeInfo.CauseToken.Should().Be(token); + } + + [Fact] + public void ExitPanicMode_WhenInPanicMode_ClearsPanicModeInfo() + { + // Arrange + var parseContext = new ParseContext(this._tokens); + var token = this._tokens.First(); + parseContext.EnterPanicMode("Panic is installed", token); + + // Act + parseContext.ExitPanicMode(); + + // Assert + parseContext.PanicMode.Should().BeFalse(); + parseContext.PanicModeInfo.Should().Be(PanicModeInfo.None); + } + + [Fact] + public void ExitPanicMode_WhenNotInPanicMode_ThrowsInvalidOperationException() + { + // Arrange + var parseContext = new ParseContext(this._tokens); + + // Act + var exception = Assert.Throws(() => parseContext.ExitPanicMode()); + + // Assert + exception.Should().NotBeNull(); + exception.Message.Should().Be("Parse operation is not in panic mode."); + } + + [Theory] + [InlineData(1, "test1")] + [InlineData(2, "test2")] + [InlineData(3, "test3")] + [InlineData(4, "test3")] + public void GetCurrentToken_Conditions_ReturnsToken(int numberOfMoves, string expected) + { + // Arrange + var parseContext = new ParseContext(this._tokens); + for (int i = 0; i < numberOfMoves; i++) + { + parseContext.MoveNext(); + } + + // Act + var actual = parseContext.GetCurrentToken(); + + // Assert + actual.Should().NotBeNull(); + actual.Lexeme.Should().Be(expected); + } + + [Fact] + public void GetCurrentToken_NeverCalledMoveNext_ThrowsInvalidOperationException() + { + // Arrange + var parseContext = new ParseContext(this._tokens); + + // Act + var exception = Assert.Throws(() => parseContext.GetCurrentToken()); + + // Assert + exception.Should().NotBeNull(); + exception.Message.Should().Be("Must invoke MoveNext() first."); + } + + [Theory] + [InlineData(1, "test2")] + [InlineData(2, "test3")] + [InlineData(3, "test3")] + [InlineData(4, "test3")] + public void GetNextToken_Conditions_ReturnsToken(int numberOfMoves, string expected) + { + // Arrange + var parseContext = new ParseContext(this._tokens); + for (int i = 0; i < numberOfMoves; i++) + { + parseContext.MoveNext(); + } + + // Act + var actual = parseContext.GetNextToken(); + + // Assert + actual.Should().NotBeNull(); + actual.Lexeme.Should().Be(expected); + } + + [Theory] + [InlineData(1, false)] + [InlineData(2, false)] + [InlineData(3, true)] + [InlineData(4, true)] + public void IsEof_Conditions_ReturnsBoolean(int numberOfMoves, bool expectedResult) + { + // Arrange + var parseContext = new ParseContext(this._tokens); + for (int i = 0; i < numberOfMoves; i++) + { + parseContext.MoveNext(); + } + + // Act + var actual = parseContext.IsEof(); + + // Assert + actual.Should().Be(expectedResult); + } + + [Theory] + [InlineData(1, 0, TokenType.STRING, true)] + [InlineData(1, 1, TokenType.STRING, true)] + [InlineData(1, 2, TokenType.STRING, false)] + [InlineData(1, 2, TokenType.EOF, true)] + [InlineData(1, 3, TokenType.EOF, true)] + public void IsMatchAtOffsetFromCurrent_Conditions_ReturnsBoolean(int numberOfMoves, int offset, object tokenType, bool expectedResult) + { + // Arrange + var parseContext = new ParseContext(this._tokens); + for (int i = 0; i < numberOfMoves; i++) + { + parseContext.MoveNext(); + } + + // Act + var actual = parseContext.IsMatchAtOffsetFromCurrent(offset, (TokenType)tokenType); + + // Assert + actual.Should().Be(expectedResult); + } + + [Fact] + public void IsMatchAtOffsetFromCurrent_InvalidOffset_ThrowsArgumentOutOfRangeException() + { + // Arrange + var parseContext = new ParseContext(this._tokens); + + // Act + var exception = Assert.Throws(() => parseContext.IsMatchAtOffsetFromCurrent(0, TokenType.VAR)); + + // Assert + exception.Should().NotBeNull(); + exception.Message.Should().Contain("Offset must be zero or greater."); + } + + [Theory] + [InlineData(1, TokenType.STRING, true)] + [InlineData(3, TokenType.STRING, false)] + [InlineData(1, TokenType.EOF, false)] + public void IsMatchCurrentToken_Conditions_ReturnsBoolean(int numberOfMoves, object tokenType, bool expectedResult) + { + // Arrange + var parseContext = new ParseContext(this._tokens); + for (int i = 0; i < numberOfMoves; i++) + { + parseContext.MoveNext(); + } + + // Act + var actual = parseContext.IsMatchCurrentToken((TokenType)tokenType); + + // Assert + actual.Should().Be(expectedResult); + } + + [Theory] + [InlineData(1, TokenType.STRING, true)] + [InlineData(2, TokenType.STRING, false)] + [InlineData(1, TokenType.EOF, false)] + public void IsMatchNextToken_Conditions_ReturnsBoolean(int numberOfMoves, object tokenType, bool expectedResult) + { + // Arrange + var parseContext = new ParseContext(this._tokens); + for (int i = 0; i < numberOfMoves; i++) + { + parseContext.MoveNext(); + } + + // Act + var actual = parseContext.IsMatchNextToken((TokenType)tokenType); + + // Assert + actual.Should().Be(expectedResult); + } + + [Theory] + [InlineData(1, true)] + [InlineData(2, true)] + [InlineData(3, true)] + [InlineData(4, false)] + public void MoveNext_Conditions_ReturnsBoolean(int numberOfMoves, bool expectedResult) + { + // Arrange + var parseContext = new ParseContext(this._tokens); + + // Act + bool? result = null; + for (int i = 0; i < numberOfMoves; i++) + { + result = parseContext.MoveNext(); + } + + // Assert + result.Should().Be(expectedResult); + } + + [Theory] + [InlineData(1, TokenType.STRING, true)] + [InlineData(2, TokenType.STRING, true)] + [InlineData(3, TokenType.EOF, false)] + [InlineData(1, TokenType.NUMBER, false)] + public void MoveNextIfCurrentToken_Conditions_ReturnsBoolean(int numberOfMoves, object tokenType, bool expectedResult) + { + // Arrange + var parseContext = new ParseContext(this._tokens); + for (int i = 0; i < numberOfMoves; i++) + { + parseContext.MoveNext(); + } + + // Act + var result = parseContext.MoveNextIfCurrentToken((TokenType)tokenType); + + // Assert + result.Should().Be(expectedResult); + } + + [Theory] + [InlineData(1, TokenType.STRING, true)] + [InlineData(2, TokenType.STRING, false)] + [InlineData(2, TokenType.EOF, true)] + [InlineData(1, TokenType.NUMBER, false)] + public void MoveNextIfNextToken_Conditions_ReturnsBoolean(int numberOfMoves, object tokenType, bool expectedResult) + { + // Arrange + var parseContext = new ParseContext(this._tokens); + for (int i = 0; i < numberOfMoves; i++) + { + parseContext.MoveNext(); + } + + // Act + var result = parseContext.MoveNextIfNextToken((TokenType)tokenType); + + // Assert + result.Should().Be(expectedResult); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Parse/ParseStrategyPoolTests.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Parse/ParseStrategyPoolTests.cs new file mode 100644 index 00000000..e6ded2b7 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Parse/ParseStrategyPoolTests.cs @@ -0,0 +1,118 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Parse +{ + using System; + using FluentAssertions; + using Rules.Framework.Rql.Pipeline.Parse; + using Xunit; + + public class ParseStrategyPoolTests + { + [Fact] + public void GetExpressionParseStrategy_GivenNotPooledStrategy_CreatesNewStrategyInstanceAndReturns() + { + // Arrange + var parseStrategyPool = new ParseStrategyPool(); + var before = DateTime.UtcNow; + + // Act + var actual = parseStrategyPool.GetExpressionParseStrategy(); + + // Assert + actual.Should().NotBeNull(); + actual.CreationDateTime.Should().BeOnOrAfter(before); + actual.ParseStrategyProvider.Should().BeSameAs(parseStrategyPool); + } + + [Fact] + public void GetExpressionParseStrategy_GivenPooledStrategy_ReturnsPooledStrategy() + { + // Arrange + var parseStrategyPool = new ParseStrategyPool(); + + // Simulate parse strategy already pooled + parseStrategyPool.GetExpressionParseStrategy(); + + var before = DateTime.UtcNow; + + // Act + var actual = parseStrategyPool.GetExpressionParseStrategy(); + + // Assert + actual.Should().NotBeNull(); + actual.CreationDateTime.Should().BeOnOrBefore(before); + actual.ParseStrategyProvider.Should().BeSameAs(parseStrategyPool); + } + + [Fact] + public void GetSegmentParseStrategy_GivenNotPooledStrategy_CreatesNewStrategyInstanceAndReturns() + { + // Arrange + var parseStrategyPool = new ParseStrategyPool(); + var before = DateTime.UtcNow; + + // Act + var actual = parseStrategyPool.GetSegmentParseStrategy(); + + // Assert + actual.Should().NotBeNull(); + actual.CreationDateTime.Should().BeOnOrAfter(before); + actual.ParseStrategyProvider.Should().BeSameAs(parseStrategyPool); + } + + [Fact] + public void GetSegmentParseStrategy_GivenPooledStrategy_ReturnsPooledStrategy() + { + // Arrange + var parseStrategyPool = new ParseStrategyPool(); + + // Simulate parse strategy already pooled + parseStrategyPool.GetSegmentParseStrategy(); + + var before = DateTime.UtcNow; + + // Act + var actual = parseStrategyPool.GetSegmentParseStrategy(); + + // Assert + actual.Should().NotBeNull(); + actual.CreationDateTime.Should().BeOnOrBefore(before); + actual.ParseStrategyProvider.Should().BeSameAs(parseStrategyPool); + } + + [Fact] + public void GetStatementParseStrategy_GivenNotPooledStrategy_CreatesNewStrategyInstanceAndReturns() + { + // Arrange + var parseStrategyPool = new ParseStrategyPool(); + var before = DateTime.UtcNow; + + // Act + var actual = parseStrategyPool.GetStatementParseStrategy(); + + // Assert + actual.Should().NotBeNull(); + actual.CreationDateTime.Should().BeOnOrAfter(before); + actual.ParseStrategyProvider.Should().BeSameAs(parseStrategyPool); + } + + [Fact] + public void GetStatementParseStrategy_GivenPooledStrategy_ReturnsPooledStrategy() + { + // Arrange + var parseStrategyPool = new ParseStrategyPool(); + + // Simulate parse strategy already pooled + parseStrategyPool.GetStatementParseStrategy(); + + var before = DateTime.UtcNow; + + // Act + var actual = parseStrategyPool.GetStatementParseStrategy(); + + // Assert + actual.Should().NotBeNull(); + actual.CreationDateTime.Should().BeOnOrBefore(before); + actual.ParseStrategyProvider.Should().BeSameAs(parseStrategyPool); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Parse/StubParseStrategy.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Parse/StubParseStrategy.cs new file mode 100644 index 00000000..6b7472a3 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Parse/StubParseStrategy.cs @@ -0,0 +1,26 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Parse +{ + using System; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Ast.Segments; + using Rules.Framework.Rql.Ast.Statements; + using Rules.Framework.Rql.Pipeline.Parse; + + internal class StubParseStrategy : IExpressionParseStrategy, ISegmentParseStrategy, IStatementParseStrategy + { + public StubParseStrategy(IParseStrategyProvider parseStrategyProvider) + { + this.CreationDateTime = DateTime.UtcNow; + this.ParseStrategyProvider = parseStrategyProvider; + } + + public DateTime CreationDateTime { get; } + public IParseStrategyProvider ParseStrategyProvider { get; } + + Expression IParseStrategy.Parse(ParseContext parseContext) => throw new NotImplementedException("Implementation not needed for testing"); + + Segment IParseStrategy.Parse(ParseContext parseContext) => throw new NotImplementedException("Implementation not needed for testing"); + + Statement IParseStrategy.Parse(ParseContext parseContext) => throw new NotImplementedException("Implementation not needed for testing"); + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Scan/ScanContextTests.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Scan/ScanContextTests.cs new file mode 100644 index 00000000..add02bfa --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Scan/ScanContextTests.cs @@ -0,0 +1,205 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Scan +{ + using System; + using FluentAssertions; + using Rules.Framework.Rql.Pipeline.Scan; + using Xunit; + + public class ScanContextTests + { + private readonly string source; + + public ScanContextTests() + { + this.source = "MATCH ONE RULE FOR \"Test\" ON $2023-01-01Z$;\nMATCH ONE RULE FOR \"Other\nTest\" ON $2024-01-01Z$;"; + } + + [Fact] + public void BeginTokenCandidate_AlreadyHasTokenCandidateCreated_ThrowsInvalidOperationException() + { + // Arrange + var scanContext = new ScanContext(this.source); + _ = scanContext.BeginTokenCandidate(); + + // Act + var actual = Assert.Throws(() => scanContext.BeginTokenCandidate()); + + // Assert + actual.Should().NotBeNull(); + actual.Message.Should().Be("A token candidate is currently created. Cannot begin a new one."); + } + + [Fact] + public void BeginTokenCandidate_NoTokenCandidateCreated_ReturnsDisposableScope() + { + // Arrange + var scanContext = new ScanContext(this.source); + + // Act + var tokenCandidateScope = scanContext.BeginTokenCandidate(); + + // Assert + tokenCandidateScope.Should().NotBeNull() + .And.BeAssignableTo(); + scanContext.TokenCandidate.Should().NotBeNull(); + scanContext.TokenCandidate.BeginPosition.Column.Should().Be(0); + scanContext.TokenCandidate.BeginPosition.Line.Should().Be(1); + scanContext.TokenCandidate.StartOffset.Should().Be(0); + scanContext.TokenCandidate.EndOffset.Should().Be(0); + scanContext.TokenCandidate.EndPosition.Column.Should().Be(0); + scanContext.TokenCandidate.EndPosition.Line.Should().Be(1); + } + + [Fact] + public void ExtractLexeme_NoTokenCandidate_ThrowsInvalidOperationException() + { + // Arrange + var scanContext = new ScanContext(this.source); + for (int i = 0; i < 4; i++) + { + _ = scanContext.MoveNext(); + } + + // Act + var actual = Assert.Throws(() => scanContext.ExtractLexeme()); + + // Assert + actual.Should().BeOfType(); + actual.Message.Should().Be("Must be on a token candidate scope. Ensure you have invoked BeginTokenCandidate() " + + "and extract lexeme before disposing of token candidate."); + } + + [Theory] + [InlineData(0, 4, "MATCH")] // Token without newline + [InlineData(63, 11, "\"Other\nTest\"")] // Token with newline + public void ExtractLexeme_TokenCandidateCreated_ReturnsTokenStringRepresentation(int numberOfMoves, int numberOfChars, string expected) + { + // Arrange + var scanContext = new ScanContext(this.source); + for (int i = 0; i < numberOfMoves; i++) + { + _ = scanContext.MoveNext(); + } + + using (scanContext.BeginTokenCandidate()) + { + for (int i = 0; i < numberOfChars; i++) + { + _ = scanContext.MoveNext(); + } + + // Act + var actual = scanContext.ExtractLexeme(); + + // Assert + actual.Should().Be(expected); + } + } + + [Theory] + [InlineData(0, 'M')] + [InlineData(2, 'T')] + [InlineData(10, 'R')] + [InlineData(93, ';')] + public void GetCurrentChar_Conditions_ReturnsChar(int numberOfMoves, char expected) + { + // Arrange + var scanContext = new ScanContext(this.source); + for (int i = 0; i < numberOfMoves; i++) + { + _ = scanContext.MoveNext(); + } + + // Act + var actual = scanContext.GetCurrentChar(); + + // Assert + actual.Should().Be(expected); + } + + [Theory] + [InlineData(0, 'A')] + [InlineData(2, 'C')] + [InlineData(10, 'U')] + [InlineData(93, '\0')] + public void GetNextChar_Conditions_ReturnsChar(int numberOfMoves, char expected) + { + // Arrange + var scanContext = new ScanContext(this.source); + for (int i = 0; i < numberOfMoves; i++) + { + _ = scanContext.MoveNext(); + } + + // Act + var actual = scanContext.GetNextChar(); + + // Assert + actual.Should().Be(expected); + } + + [Theory] + [InlineData(0, false)] + [InlineData(2, false)] + [InlineData(93, true)] + public void IsEof_Conditions_ReturnsBool(int numberOfMoves, bool expected) + { + // Arrange + var scanContext = new ScanContext(this.source); + for (int i = 0; i < numberOfMoves; i++) + { + _ = scanContext.MoveNext(); + } + + // Act + var actual = scanContext.IsEof(); + + // Assert + actual.Should().Be(expected); + } + + [Theory] + [InlineData(0, true, 1)] + [InlineData(2, true, 3)] + [InlineData(92, false, 92)] + public void MoveNext_Conditions_ReturnsBool(int numberOfMoves, bool expected, int expectedOffset) + { + // Arrange + var scanContext = new ScanContext(this.source); + for (int i = 0; i < numberOfMoves; i++) + { + _ = scanContext.MoveNext(); + } + + // Act + var actual = scanContext.MoveNext(); + + // Assert + actual.Should().Be(expected); + scanContext.Offset.Should().Be(expectedOffset); + } + + [Theory] + [InlineData(0, 'A', true, 1)] + [InlineData(0, 'T', false, 0)] + [InlineData(2, 'C', true, 3)] + [InlineData(2, 'H', false, 2)] + [InlineData(92, '\0', false, 92)] + public void MoveNextConditionally_Conditions_ReturnsBool(int numberOfMoves, char nextChar, bool expected, int expectedOffset) + { + // Arrange + var scanContext = new ScanContext(this.source); + for (int i = 0; i < numberOfMoves; i++) + { + _ = scanContext.MoveNext(); + } + + // Act + var actual = scanContext.MoveNextConditionally(nextChar); + + // Assert + actual.Should().Be(expected); + scanContext.Offset.Should().Be(expectedOffset); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Scan/TokenCandidateInfoTests.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Scan/TokenCandidateInfoTests.cs new file mode 100644 index 00000000..507cc6e4 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Scan/TokenCandidateInfoTests.cs @@ -0,0 +1,71 @@ +namespace Rules.Framework.Rql.Tests.Pipeline.Scan +{ + using System; + using FluentAssertions; + using Rules.Framework.Rql.Pipeline.Scan; + using Xunit; + + public class TokenCandidateInfoTests + { + [Fact] + public void MarkAsError_GivenAlreadyError_ThrowsInvalidOperationException() + { + // Arrange + var tokenCandidateInfo = new TokenCandidateInfo(10, 2, 3); + tokenCandidateInfo.MarkAsError("Existent error"); + + // Act + var actual = Assert.Throws(() => tokenCandidateInfo.MarkAsError("Test error")); + + // Assert + actual.Should().NotBeNull(); + actual.Message.Should().Be("An error has already been reported for specified token candidate."); + } + + [Fact] + public void MarkAsError_GivenNoError_MarksAsErrorAndSetsMessage() + { + // Arrange + var tokenCandidateInfo = new TokenCandidateInfo(10, 2, 3); + + // Act + tokenCandidateInfo.MarkAsError("Test error"); + + // Assert + tokenCandidateInfo.HasError.Should().BeTrue(); + tokenCandidateInfo.Message.Should().Be("Test error"); + } + + [Fact] + public void NextColumn_NoConditions_IncreasesEndOffsetAndColumnCount() + { + // Arrange + var tokenCandidateInfo = new TokenCandidateInfo(10, 2, 3); + + // Act + tokenCandidateInfo.NextColumn(); + + // Assert + tokenCandidateInfo.EndOffset.Should().Be(11); + tokenCandidateInfo.EndPosition.Column.Should().Be(4); + tokenCandidateInfo.EndPosition.Line.Should().Be(2); + tokenCandidateInfo.Length.Should().Be(2); + } + + [Fact] + public void NextLine_NoConditions_IncreasesEndOffsetAndLineCount() + { + // Arrange + var tokenCandidateInfo = new TokenCandidateInfo(10, 2, 3); + + // Act + tokenCandidateInfo.NextLine(); + + // Assert + tokenCandidateInfo.EndOffset.Should().Be(11); + tokenCandidateInfo.EndPosition.Column.Should().Be(1); + tokenCandidateInfo.EndPosition.Line.Should().Be(3); + tokenCandidateInfo.Length.Should().Be(2); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/ReverseRqlBuilderTests.cs b/tests/Rules.Framework.Rql.Tests/ReverseRqlBuilderTests.cs new file mode 100644 index 00000000..b7b070fd --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/ReverseRqlBuilderTests.cs @@ -0,0 +1,658 @@ +namespace Rules.Framework.Rql.Tests +{ + using FluentAssertions; + using Moq; + using Rules.Framework.Rql.Ast; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Ast.Segments; + using Rules.Framework.Rql.Ast.Statements; + using Rules.Framework.Rql.Tokens; + using Xunit; + + public class ReverseRqlBuilderTests + { + [Theory] + [InlineData("expression")] + [InlineData("segment")] + [InlineData("statement")] + public void BuildRql_GivenKnownAstElement_ReturnsRqlRepresentation(string astElementType) + { + // Arrange + IAstElement astElement; + switch (astElementType) + { + case "expression": + var expression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(expression) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("test"); + astElement = expression; + break; + + case "segment": + var segment = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(segment) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("test"); + astElement = segment; + break; + + case "statement": + var statement = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(statement) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("test"); + astElement = statement; + break; + + default: + throw new NotImplementedException(); + } + + var builder = new ReverseRqlBuilder(); + + // Act + var actual = builder.BuildRql(astElement); + + // Assert + actual.Should().Be("test"); + } + + [Fact] + public void BuildRql_GivenNullAstElement_ThrowsNotSupportedException() + { + // Arrange + var builder = new ReverseRqlBuilder(); + + // Act + var actual = Assert.Throws(() => builder.BuildRql(null)); + + // Assert + actual.ParamName.Should().Be("astElement"); + } + + [Fact] + public void BuildRql_GivenUnknownAstElement_ThrowsNotSupportedException() + { + // Arrange + var unknownAstElement = CreateMock(); + + var builder = new ReverseRqlBuilder(); + + // Act + var actual = Assert.Throws(() => builder.BuildRql(unknownAstElement)); + + // Assert + actual.Message.Should().Contain("The given AST element is not supported:"); + } + + [Fact] + public void VisitAssignmentExpression_GivenAssignmentExpression_ReturnsRqlRepresentation() + { + // Arrange + var left = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(left) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("var1"); + var right = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(right) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("array { 1, 2, 3 }"); + var @operator = Token.Create("=", false, null, RqlSourcePosition.Empty, RqlSourcePosition.Empty, 1, TokenType.EQUAL); + var assignmentExpression = new AssignmentExpression(left, @operator, right); + + var builder = new ReverseRqlBuilder(); + + // Act + var actual = builder.VisitAssignmentExpression(assignmentExpression); + + // Assert + actual.Should().Be("var1 = array { 1, 2, 3 }"); + } + + [Fact] + public void VisitBinaryExpression_GivenBinaryExpression_ReturnsRqlRepresentation() + { + // Arrange + var left = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(left) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("1"); + var right = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(right) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("{ 1, 2, 3 }"); + var @operator = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(@operator) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("in"); + var binaryExpression = new BinaryExpression(left, @operator, right); + + var builder = new ReverseRqlBuilder(); + + // Act + var actual = builder.VisitBinaryExpression(binaryExpression); + + // Assert + actual.Should().Be("1 in { 1, 2, 3 }"); + } + + [Fact] + public void VisitCardinalitySegment_GivenCardinalitySegment_ReturnsRqlRepresentation() + { + // Arrange + var cardinalityKeyword = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(cardinalityKeyword) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("one"); + var ruleKeyword = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(ruleKeyword) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("rule"); + var cardinalitySegment = CardinalitySegment.Create(cardinalityKeyword, ruleKeyword); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitCardinalitySegment(cardinalitySegment); + + // Assert + actual.Should().Be("one rule"); + } + + [Fact] + public void VisitExpressionStatement_GivenExpressionStatement_ReturnsRqlRepresentation() + { + // Arrange + var expression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(expression) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("MATCH ONE RULE FOR \"Test\" ON $2023-01-01Z$"); + var expressionStatement = ExpressionStatement.Create(expression); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitExpressionStatement(expressionStatement); + + // Assert + actual.Should().Be("MATCH ONE RULE FOR \"Test\" ON $2023-01-01Z$;"); + } + + [Fact] + public void VisitIdentifierExpression_GivenIdentifierExpression_ReturnsRqlRepresentation() + { + // Arrange + var identifierToken = Token.Create("abc", false, null, RqlSourcePosition.Empty, RqlSourcePosition.Empty, 3, TokenType.IDENTIFIER); + var identifierExpression = new IdentifierExpression(identifierToken); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitIdentifierExpression(identifierExpression); + + // Assert + actual.Should().Be("abc"); + } + + [Fact] + public void VisitInputConditionSegment_GivenInputConditionSegment_ReturnsRqlRepresentation() + { + // Arrange + var leftExpression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(leftExpression) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("@TestCondition"); + var operatorToken = Token.Create("is", false, null, RqlSourcePosition.Empty, RqlSourcePosition.Empty, 2, TokenType.IS); + var rightExpression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(rightExpression) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("true"); + var inputConditionSegment = new InputConditionSegment(leftExpression, operatorToken, rightExpression); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitInputConditionSegment(inputConditionSegment); + + // Assert + actual.Should().Be("@TestCondition is true"); + } + + [Fact] + public void VisitInputConditionsSegment_GivenInputConditionsSegmentWithConditions_ReturnsRqlRepresentation() + { + // Arrange + var inputConditionSegment1 = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(inputConditionSegment1) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("@TestCondition1 is true"); + var inputConditionSegment2 = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(inputConditionSegment2) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("@TestCondition2 is 30"); + var inputConditionsSegment = new InputConditionsSegment(new[] { inputConditionSegment1, inputConditionSegment2 }); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitInputConditionsSegment(inputConditionsSegment); + + // Assert + actual.Should().Be("WITH { @TestCondition1 is true, @TestCondition2 is 30 }"); + } + + [Fact] + public void VisitInputConditionsSegment_GivenInputConditionsSegmentWithoutConditions_ReturnsRqlRepresentation() + { + // Arrange + var inputConditionsSegment = new InputConditionsSegment(new Segment[0]); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitInputConditionsSegment(inputConditionsSegment); + + // Assert + actual.Should().Be(string.Empty); + } + + [Fact] + public void VisitKeywordExpression_GivenKeywordExpression_ReturnsRqlRepresentation() + { + // Arrange + var keywordToken = Token.Create("CREATE", false, null, RqlSourcePosition.Empty, RqlSourcePosition.Empty, 6, TokenType.CREATE); + var keyworkExpression = KeywordExpression.Create(keywordToken); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitKeywordExpression(keyworkExpression); + + // Assert + actual.Should().Be("CREATE"); + } + + [Theory] + [InlineData(LiteralType.Bool, true, "TRUE")] + [InlineData(LiteralType.Decimal, 10.35, "10,35")] + [InlineData(LiteralType.Integer, 3, "3")] + [InlineData(LiteralType.String, "test", "test")] + [InlineData(LiteralType.DateTime, "2024-01-05T22:36:05Z", "$2024-01-05T22:36:05Z$")] + [InlineData(LiteralType.Undefined, "abc", "abc")] + public void VisitLiteralExpression_GivenLiteralExpression_ReturnsRqlRepresentation(object literalType, object value, string expected) + { + // Arrange + var value1 = (LiteralType)literalType == LiteralType.DateTime ? DateTime.Parse(value.ToString()) : value; + var token = Token.Create(value1.ToString(), false, value1, RqlSourcePosition.Empty, RqlSourcePosition.Empty, 6, TokenType.CREATE); + var literalExpression = LiteralExpression.Create((LiteralType)literalType, token, value1); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitLiteralExpression(literalExpression); + + // Assert + actual.Should().Be(expected); + } + + [Fact] + public void VisitMatchExpression_GivenMatchExpressionWithConditions_ReturnsRqlRepresentation() + { + // Arrange + var cardinalitySegment = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(cardinalitySegment) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("ONE RULE"); + var contentTypeExpression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(contentTypeExpression) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("\"Test\""); + var matchDateExpression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(matchDateExpression) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("$2024-03-24$"); + var inputConditionsSegment = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(inputConditionsSegment) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("WITH { @TestCondition1 is true }"); + var matchExpression = MatchExpression.Create(cardinalitySegment, contentTypeExpression, matchDateExpression, inputConditionsSegment); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitMatchExpression(matchExpression); + + // Assert + actual.Should().Be("MATCH ONE RULE FOR \"Test\" ON $2024-03-24$ WITH { @TestCondition1 is true }"); + } + + [Fact] + public void VisitMatchExpression_GivenMatchExpressionWithoutConditions_ReturnsRqlRepresentation() + { + // Arrange + var cardinalitySegment = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(cardinalitySegment) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("ONE RULE"); + var contentTypeExpression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(contentTypeExpression) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("\"Test\""); + var matchDateExpression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(matchDateExpression) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("$2024-03-24$"); + var inputConditionsSegment = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(inputConditionsSegment) + .Setup(x => x.Accept(It.IsAny>())) + .Returns(string.Empty); + var matchExpression = MatchExpression.Create(cardinalitySegment, contentTypeExpression, matchDateExpression, inputConditionsSegment); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitMatchExpression(matchExpression); + + // Assert + actual.Should().Be("MATCH ONE RULE FOR \"Test\" ON $2024-03-24$"); + } + + [Fact] + public void VisitNewArrayExpression_GivenNewArrayExpressionWithElementsInitializer_ReturnsRqlRepresentation() + { + // Arrange + var arrayToken = Token.Create("ARRAY", false, null, RqlSourcePosition.Empty, RqlSourcePosition.Empty, 5, TokenType.ARRAY); + var initializerBeginToken = Token.Create("{", false, null, RqlSourcePosition.Empty, RqlSourcePosition.Empty, 1, TokenType.BRACE_LEFT); + var sizeExpression = Expression.None; + var value1Expression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(value1Expression) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("\"abc\""); + var value2Expression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(value2Expression) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("123"); + var values = new[] + { + value1Expression, value2Expression + }; + var initializerEndToken = Token.Create("}", false, null, RqlSourcePosition.Empty, RqlSourcePosition.Empty, 1, TokenType.BRACE_RIGHT); + + var newArrayExpression = NewArrayExpression.Create(arrayToken, initializerBeginToken, sizeExpression, values, initializerEndToken); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitNewArrayExpression(newArrayExpression); + + // Assert + actual.Should().Be("ARRAY { \"abc\", 123 }"); + } + + [Fact] + public void VisitNewArrayExpression_GivenNewArrayExpressionWithSizeInitializer_ReturnsRqlRepresentation() + { + // Arrange + var arrayToken = Token.Create("ARRAY", false, null, RqlSourcePosition.Empty, RqlSourcePosition.Empty, 5, TokenType.ARRAY); + var initializerBeginToken = Token.Create("[", false, null, RqlSourcePosition.Empty, RqlSourcePosition.Empty, 1, TokenType.STRAIGHT_BRACKET_LEFT); + var sizeExpression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(sizeExpression) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("3"); + var value1Expression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(value1Expression) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("\"test\""); + var values = new[] + { + value1Expression + }; + var initializerEndToken = Token.Create("]", false, null, RqlSourcePosition.Empty, RqlSourcePosition.Empty, 1, TokenType.STRAIGHT_BRACKET_RIGHT); + + var newArrayExpression = NewArrayExpression.Create(arrayToken, initializerBeginToken, sizeExpression, values, initializerEndToken); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitNewArrayExpression(newArrayExpression); + + // Assert + actual.Should().Be("ARRAY [3]"); + } + + [Fact] + public void VisitNewObjectExpression_GivenNewObjectExpressionWithInitializer_ReturnsRqlRepresentation() + { + // Arrange + var objectToken = Token.Create("OBJECT", false, null, RqlSourcePosition.Empty, RqlSourcePosition.Empty, 6, TokenType.OBJECT); + var propertyAssignment1Expression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(propertyAssignment1Expression) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("\"Name\" = \"John Doe\""); + var propertyAssignment2Expression = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(propertyAssignment2Expression) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("\"Age\" = 1"); + var values = new[] { propertyAssignment1Expression, propertyAssignment2Expression }; + + var newObjectExpression = new NewObjectExpression(objectToken, values); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitNewObjectExpression(newObjectExpression); + + // Assert + actual.Should().Be($"OBJECT{Environment.NewLine}{{{Environment.NewLine} \"Name\" = \"John Doe\",{Environment.NewLine} \"Age\" = 1{Environment.NewLine}}}"); + } + + [Fact] + public void VisitNewObjectExpression_GivenNewObjectExpressionWithoutInitializer_ReturnsRqlRepresentation() + { + // Arrange + var objectToken = Token.Create("OBJECT", false, null, RqlSourcePosition.Empty, RqlSourcePosition.Empty, 6, TokenType.OBJECT); + var values = new Expression[0]; + + var newObjectExpression = new NewObjectExpression(objectToken, values); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitNewObjectExpression(newObjectExpression); + + // Assert + actual.Should().Be("OBJECT"); + } + + [Fact] + public void VisitNoneExpression_GivenNoneExpression_ReturnsRqlRepresentation() + { + // Arrange + var noneExpression = new NoneExpression(); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.BuildRql(noneExpression); + + // Assert + actual.Should().BeEmpty(); + } + + [Fact] + public void VisitNoneSegment_GivenNoneSegment_ReturnsRqlRepresentation() + { + // Arrange + var noneSegment = new NoneSegment(); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.BuildRql(noneSegment); + + // Assert + actual.Should().BeEmpty(); + } + + [Fact] + public void VisitNoneStatement_GivenNoneStatement_ReturnsRqlRepresentation() + { + // Arrange + var noneStatement = new NoneStatement(); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.BuildRql(noneStatement); + + // Assert + actual.Should().BeEmpty(); + } + + [Theory] + [InlineData(new[] { "NOT", "IN" }, new object[] { TokenType.NOT, TokenType.IN }, "NOT IN")] + [InlineData(new[] { "=" }, new object[] { TokenType.EQUAL }, "=")] + public void VisitOperatorSegment_GivenOperatorSegment_ReturnsRqlRepresentation(string[] operatorTokens, object[] tokenTypes, string expected) + { + // Act + var tokens = new Token[operatorTokens.Length]; + for (int i = 0; i < operatorTokens.Length; i++) + { + tokens[i] = Token.Create( + operatorTokens[i], + false, + null, + RqlSourcePosition.Empty, + RqlSourcePosition.Empty, + (uint)operatorTokens[i].Length, + (TokenType)tokenTypes[i]); + } + + var operatorSegment = new OperatorSegment(tokens); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitOperatorSegment(operatorSegment); + + // Assert + actual.Should().Be(expected); + } + + [Fact] + public void VisitPlaceholderExpression_GivenPlaceholderExpression_ReturnsRqlRepresentation() + { + // Arrange + var placeholderToken = Token.Create("@test", false, null, RqlSourcePosition.Empty, RqlSourcePosition.Empty, 5, TokenType.PLACEHOLDER); + + var placeholderExpression = new PlaceholderExpression(placeholderToken); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitPlaceholderExpression(placeholderExpression); + + // Assert + actual.Should().Be("@test"); + } + + [Fact] + public void VisitSearchExpression_GivenSearchExpressionWithInputConditions_ReturnsRqlRepresentation() + { + // Arrange + var contentType = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(contentType) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("\"test content type\""); + + var dateBegin = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(dateBegin) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("$2023-01-01$"); + + var dateEnd = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(dateEnd) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("$2024-01-01$"); + + var inputConditions = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(inputConditions) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("WITH { @TestCondition1 is \"abc\" }"); + + var searchExpression = new SearchExpression(contentType, dateBegin, dateEnd, inputConditions); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitSearchExpression(searchExpression); + + // Assert + actual.Should().Be("SEARCH RULES FOR \"test content type\" SINCE $2023-01-01$ UNTIL $2024-01-01$ WITH { @TestCondition1 is \"abc\" }"); + } + + [Fact] + public void VisitSearchExpression_GivenSearchExpressionWithoutInputConditions_ReturnsRqlRepresentation() + { + // Arrange + var contentType = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(contentType) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("\"test content type\""); + + var dateBegin = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(dateBegin) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("$2023-01-01$"); + + var dateEnd = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(dateEnd) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("$2024-01-01$"); + + var inputConditions = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(inputConditions) + .Setup(x => x.Accept(It.IsAny>())) + .Returns(string.Empty); + + var searchExpression = new SearchExpression(contentType, dateBegin, dateEnd, inputConditions); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitSearchExpression(searchExpression); + + // Assert + actual.Should().Be("SEARCH RULES FOR \"test content type\" SINCE $2023-01-01$ UNTIL $2024-01-01$"); + } + + [Fact] + public void VisitUnaryExpression_GivenUnaryExpression_ReturnsRqlRepresentation() + { + // Arrange + var unaryOperator = Token.Create("-", false, null, RqlSourcePosition.Empty, RqlSourcePosition.Empty, 1, TokenType.MINUS); + var right = CreateMock(RqlSourcePosition.Empty, RqlSourcePosition.Empty); + Mock.Get(right) + .Setup(x => x.Accept(It.IsAny>())) + .Returns("123"); + + var unaryExpression = new UnaryExpression(unaryOperator, right); + + var reverseRqlBuilder = new ReverseRqlBuilder(); + + // Act + var actual = reverseRqlBuilder.VisitUnaryExpression(unaryExpression); + + // Assert + actual.Should().Be("-123"); + } + + private T CreateMock(params object[] args) + where T : class + { + var mock = new Mock(args); + return mock.Object; + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/RqlEngineBuilderTests.cs b/tests/Rules.Framework.Rql.Tests/RqlEngineBuilderTests.cs new file mode 100644 index 00000000..d482f21a --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/RqlEngineBuilderTests.cs @@ -0,0 +1,54 @@ +namespace Rules.Framework.Rql.Tests +{ + using FluentAssertions; + using Moq; + using Rules.Framework.Rql.Tests.Stubs; + using Xunit; + + public class RqlEngineBuilderTests + { + [Fact] + public void Build_GivenNullRqlOptions_ThrowsArgumentNullException() + { + // Arrange + var rulesEngine = Mock.Of>(); + + // Act + var argumentNullException = Assert.Throws(() => + RqlEngineBuilder.CreateRqlEngine(rulesEngine) + .WithOptions(null)); + + // Assert + argumentNullException.Should().NotBeNull(); + argumentNullException.ParamName.Should().Be("options"); + } + + [Fact] + public void Build_GivenNullRulesEngine_ThrowsArgumentNullException() + { + // Act + var argumentNullException = Assert.Throws(() => + RqlEngineBuilder.CreateRqlEngine(null)); + + // Assert + argumentNullException.Should().NotBeNull(); + argumentNullException.ParamName.Should().Be("rulesEngine"); + } + + [Fact] + public void Build_GivenRulesEngineAndRqlOptions_BuildsRqlEngine() + { + // Arrange + var rulesEngine = Mock.Of>(); + var rqlOptions = RqlOptions.NewWithDefaults(); + + // Act + var rqlEngine = RqlEngineBuilder.CreateRqlEngine(rulesEngine) + .WithOptions(rqlOptions) + .Build(); + + // Assert + rqlEngine.Should().NotBeNull(); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/RqlEngineTests.cs b/tests/Rules.Framework.Rql.Tests/RqlEngineTests.cs new file mode 100644 index 00000000..e7187564 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/RqlEngineTests.cs @@ -0,0 +1,506 @@ +namespace Rules.Framework.Rql.Tests +{ + using System.Globalization; + using System.Threading.Tasks; + using FluentAssertions; + using Moq; + using Rules.Framework.Rql.Ast.Expressions; + using Rules.Framework.Rql.Ast.Segments; + using Rules.Framework.Rql.Ast.Statements; + using Rules.Framework.Rql.Messages; + using Rules.Framework.Rql.Pipeline.Interpret; + using Rules.Framework.Rql.Pipeline.Parse; + using Rules.Framework.Rql.Pipeline.Scan; + using Rules.Framework.Rql.Runtime.Types; + using Rules.Framework.Rql.Tests.Stubs; + using Rules.Framework.Rql.Tests.TestStubs; + using Rules.Framework.Rql.Tokens; + using Xunit; + + public class RqlEngineTests + { + private IInterpreter interpreter; + private IParser parser; + private RqlEngine rqlEngine; + private IScanner scanner; + + public RqlEngineTests() + { + this.scanner = Mock.Of(); + this.parser = Mock.Of(); + this.interpreter = Mock.Of(); + var rqlEngineArgs = new RqlEngineArgs + { + Interpreter = interpreter, + Parser = parser, + Scanner = scanner, + }; + + this.rqlEngine = new RqlEngine(rqlEngineArgs); + } + + [Fact] + public void Dispose_NoConditions_ExecutesDisposal() + { + // Act + this.rqlEngine.Dispose(); + } + + /// + /// Case 1 - RQL source is given with 2 match expression statements. The first statement + /// produces a result with 'nothing' as output. The second statement produces a result with + /// 1 rule as output. + /// + [Fact] + public async Task ExecuteAsync_GivenRqlSourceCase1_InterpretsAndReturnsResult() + { + // Arrange + var rql = "MATCH ONE RULE FOR \"Test\" ON $2023-01-01Z$;\\nMATCH ONE RULE FOR \"Other\\nTest\" ON $2024-01-01Z$;"; + var tokens = new[] + { + CreateToken("MATCH", null, TokenType.MATCH), + CreateToken("ONE", null, TokenType.ONE), + CreateToken("RULE", null, TokenType.RULE), + CreateToken("FOR", null, TokenType.FOR), + CreateToken("\"Test\"", "Test", TokenType.STRING), + CreateToken("ON", null, TokenType.ON), + CreateToken("$2023-01-01Z$", DateTime.Parse("2023-01-01Z", CultureInfo.InvariantCulture), TokenType.DATE), + CreateToken(";", null, TokenType.SEMICOLON), + CreateToken("MATCH", null, TokenType.MATCH), + CreateToken("ONE", null, TokenType.ONE), + CreateToken("RULE", null, TokenType.RULE), + CreateToken("FOR", null, TokenType.FOR), + CreateToken("\"Other\\nTest\"", "Other\nTest", TokenType.STRING), + CreateToken("ON", null, TokenType.ON), + CreateToken("$2024-01-01Z$", DateTime.Parse("2024-01-01Z", CultureInfo.InvariantCulture), TokenType.DATE), + CreateToken(";", null, TokenType.SEMICOLON), + }.ToList().AsReadOnly(); + var scanResult = ScanResult.CreateSuccess(tokens, new List()); + var statements = new[] + { + ExpressionStatement.Create( + MatchExpression.Create( + CardinalitySegment.Create( + KeywordExpression.Create(tokens[1]), // ONE + KeywordExpression.Create(tokens[2])), // RULE + LiteralExpression.Create(LiteralType.String, tokens[4], tokens[4].Literal), // Test + LiteralExpression.Create(LiteralType.DateTime, tokens[6], tokens[6].Literal), // 2023-01-01Z + Segment.None)), + ExpressionStatement.Create( + MatchExpression.Create( + CardinalitySegment.Create( + KeywordExpression.Create(tokens[9]), // ONE + KeywordExpression.Create(tokens[10])), // RULE + LiteralExpression.Create(LiteralType.String, tokens[12], tokens[12].Literal), // Other\nTest + LiteralExpression.Create(LiteralType.DateTime, tokens[14], tokens[14].Literal), // 2024-01-01Z + Segment.None)), + }.ToList().AsReadOnly(); + var parseResult = ParseResult.CreateSuccess(statements, new List()); + var rqlRule = new RqlRule(); + var rqlArray = new RqlArray(1); + rqlArray.SetAtIndex(0, rqlRule); + var interpretResult = new InterpretResult(); + interpretResult.AddStatementResult(new NothingStatementResult("MATCH ONE RULE FOR \"Test\" ON $2023-01-01Z$;")); + interpretResult.AddStatementResult(new ExpressionStatementResult("MATCH ONE RULE FOR \"Other\\nTest\" ON $2024-01-01Z$;", rqlArray)); + + Mock.Get(this.scanner) + .Setup(x => x.ScanTokens(It.Is(rql, StringComparer.Ordinal))) + .Returns(scanResult); + Mock.Get(this.parser) + .Setup(x => x.Parse(It.Is>(tks => object.Equals(tks, tokens)))) + .Returns(parseResult); + Mock.Get(this.interpreter) + .Setup(x => x.InterpretAsync(It.Is>(stmts => object.Equals(stmts, statements)))) + .Returns(Task.FromResult(interpretResult)); + + // Act + var results = await this.rqlEngine.ExecuteAsync(rql); + + // Assert + Mock.VerifyAll( + Mock.Get(this.scanner), + Mock.Get(this.parser), + Mock.Get(this.interpreter)); + + results.Should().NotBeNullOrEmpty() + .And.HaveCount(2); + var result1 = results.FirstOrDefault(); + result1.Should().NotBeNull() + .And.BeOfType(); + result1.As() + .Rql.Should().Be("MATCH ONE RULE FOR \"Test\" ON $2023-01-01Z$;"); + var result2 = results.LastOrDefault(); + result2.Should().NotBeNull() + .And.BeOfType>(); + result2.As>() + .Rql.Should().Be("MATCH ONE RULE FOR \"Other\\nTest\" ON $2024-01-01Z$;"); + result2.As>() + .Lines.Should().HaveCount(1) + .And.Contain(line => line.LineNumber == 1 && object.Equals(line.Rule, rqlRule)); + } + + /// + /// Case 2 - RQL source is given with 1 new array expression statement which produces as + /// result an empty array with size 3. + /// + [Fact] + public async Task ExecuteAsync_GivenRqlSourceCase2_InterpretsAndReturnsResult() + { + // Arrange + var rql = "ARRAY[3];"; + var tokens = new[] + { + CreateToken("ARRAY", null, TokenType.ARRAY), + CreateToken("[", null, TokenType.STRAIGHT_BRACKET_LEFT), + CreateToken("3", 3, TokenType.INT), + CreateToken("]", null, TokenType.STRAIGHT_BRACKET_RIGHT), + CreateToken(";", null, TokenType.SEMICOLON), + }.ToList().AsReadOnly(); + var scanResult = ScanResult.CreateSuccess(tokens, new List()); + var statements = new[] + { + ExpressionStatement.Create( + NewArrayExpression.Create( + tokens[0], // ARRAY + tokens[1], // [ + LiteralExpression.Create(LiteralType.Integer, tokens[2], tokens[2].Literal), // 3 + Array.Empty(), + tokens[3])), // ] + }.ToList().AsReadOnly(); + var parseResult = ParseResult.CreateSuccess(statements, new List()); + var rqlArray = new RqlArray(3); + var interpretResult = new InterpretResult(); + interpretResult.AddStatementResult(new ExpressionStatementResult(rql, rqlArray)); + + Mock.Get(this.scanner) + .Setup(x => x.ScanTokens(It.Is(rql, StringComparer.Ordinal))) + .Returns(scanResult); + Mock.Get(this.parser) + .Setup(x => x.Parse(It.Is>(tks => object.Equals(tks, tokens)))) + .Returns(parseResult); + Mock.Get(this.interpreter) + .Setup(x => x.InterpretAsync(It.Is>(stmts => object.Equals(stmts, statements)))) + .Returns(Task.FromResult(interpretResult)); + + // Act + var results = await this.rqlEngine.ExecuteAsync(rql); + + // Assert + Mock.VerifyAll( + Mock.Get(this.scanner), + Mock.Get(this.parser), + Mock.Get(this.interpreter)); + + results.Should().NotBeNullOrEmpty() + .And.HaveCount(1); + var result1 = results.FirstOrDefault(); + result1.Should().NotBeNull() + .And.BeOfType(); + result1.As() + .Rql.Should().Be(rql); + result1.As() + .Value.Should().Be(rqlArray); + result1.As().Value.As() + .Size.Value.Should().Be(3); + } + + /// + /// Case 3 - RQL source is given with 1 new array expression statement which produces as + /// result an empty array with size 0. + /// + [Fact] + public async Task ExecuteAsync_GivenRqlSourceCase3_InterpretsAndReturnsResult() + { + // Arrange + var rql = "ARRAY[0];"; + var tokens = new[] + { + CreateToken("ARRAY", null, TokenType.ARRAY), + CreateToken("[", null, TokenType.STRAIGHT_BRACKET_LEFT), + CreateToken("0", 0, TokenType.INT), + CreateToken("]", null, TokenType.STRAIGHT_BRACKET_RIGHT), + CreateToken(";", null, TokenType.SEMICOLON), + }.ToList().AsReadOnly(); + var scanResult = ScanResult.CreateSuccess(tokens, new List()); + var statements = new[] + { + ExpressionStatement.Create( + NewArrayExpression.Create( + tokens[0], // ARRAY + tokens[1], // [ + LiteralExpression.Create(LiteralType.Integer, tokens[2], tokens[2].Literal), // 0 + Array.Empty(), + tokens[3])), // ] + }.ToList().AsReadOnly(); + var parseResult = ParseResult.CreateSuccess(statements, new List()); + var rqlArray = new RqlArray(0); + var interpretResult = new InterpretResult(); + interpretResult.AddStatementResult(new ExpressionStatementResult(rql, rqlArray)); + + Mock.Get(this.scanner) + .Setup(x => x.ScanTokens(It.Is(rql, StringComparer.Ordinal))) + .Returns(scanResult); + Mock.Get(this.parser) + .Setup(x => x.Parse(It.Is>(tks => object.Equals(tks, tokens)))) + .Returns(parseResult); + Mock.Get(this.interpreter) + .Setup(x => x.InterpretAsync(It.Is>(stmts => object.Equals(stmts, statements)))) + .Returns(Task.FromResult(interpretResult)); + + // Act + var results = await this.rqlEngine.ExecuteAsync(rql); + + // Assert + Mock.VerifyAll( + Mock.Get(this.scanner), + Mock.Get(this.parser), + Mock.Get(this.interpreter)); + + results.Should().NotBeNullOrEmpty() + .And.HaveCount(1); + var result1 = results.FirstOrDefault(); + result1.Should().NotBeNull() + .And.BeOfType(); + result1.As() + .Rql.Should().Be(rql); + result1.As() + .Value.Should().Be(rqlArray); + result1.As().Value.As() + .Size.Value.Should().Be(0); + } + + /// + /// Case 4 - RQL source is given with 1 string expression statement which produces as result + /// a string. + /// + [Fact] + public async Task ExecuteAsync_GivenRqlSourceCase4_InterpretsAndReturnsResult() + { + // Arrange + var rql = "\"test string\";"; + var tokens = new[] + { + CreateToken("\"test string\"", "test string", TokenType.STRING), + CreateToken(";", null, TokenType.SEMICOLON), + }.ToList().AsReadOnly(); + var scanResult = ScanResult.CreateSuccess(tokens, new List()); + var statements = new[] + { + ExpressionStatement.Create( + LiteralExpression.Create(LiteralType.Integer, tokens[0], tokens[0].Literal)), // "test string" + }.ToList().AsReadOnly(); + var parseResult = ParseResult.CreateSuccess(statements, new List()); + var rqlString = new RqlString("test string"); + var interpretResult = new InterpretResult(); + interpretResult.AddStatementResult(new ExpressionStatementResult(rql, rqlString)); + + Mock.Get(this.scanner) + .Setup(x => x.ScanTokens(It.Is(rql, StringComparer.Ordinal))) + .Returns(scanResult); + Mock.Get(this.parser) + .Setup(x => x.Parse(It.Is>(tks => object.Equals(tks, tokens)))) + .Returns(parseResult); + Mock.Get(this.interpreter) + .Setup(x => x.InterpretAsync(It.Is>(stmts => object.Equals(stmts, statements)))) + .Returns(Task.FromResult(interpretResult)); + + // Act + var results = await this.rqlEngine.ExecuteAsync(rql); + + // Assert + Mock.VerifyAll( + Mock.Get(this.scanner), + Mock.Get(this.parser), + Mock.Get(this.interpreter)); + + results.Should().NotBeNullOrEmpty() + .And.HaveCount(1); + var result1 = results.FirstOrDefault(); + result1.Should().NotBeNull() + .And.BeOfType(); + result1.As() + .Rql.Should().Be(rql); + result1.As() + .Value.Should().Be(rqlString); + result1.As().Value.As() + .Value.Should().Be("test string"); + } + + /// + /// Case 5 - RQL source is given and fails to scan, throwing a RqlException. + /// + [Fact] + public async Task ExecuteAsync_GivenRqlSourceCase5HavingErrorsOnScanner_ThrowsRqlException() + { + // Arrange + var rql = "\"test string\";"; + var messages = new List + { + Message.Create("Sample scan error", RqlSourcePosition.From(1, 1), RqlSourcePosition.From(1, 10), MessageSeverity.Error), + }; + var scanResult = ScanResult.CreateError(messages); + + Mock.Get(this.scanner) + .Setup(x => x.ScanTokens(It.Is(rql, StringComparer.Ordinal))) + .Returns(scanResult); + + // Act + var rqlException = await Assert.ThrowsAsync(async () => await this.rqlEngine.ExecuteAsync(rql)); + + // Assert + Mock.VerifyAll( + Mock.Get(this.scanner)); + + rqlException.Message.Should().Be("Errors have occurred processing provided RQL source - Sample scan error for source @{1:1}-{1:10}"); + rqlException.Errors.Should().HaveCount(1); + var rqlError = rqlException.Errors.First(); + rqlError.Text.Should().Be("Sample scan error"); + rqlError.BeginPosition.Should().Be(RqlSourcePosition.From(1, 1)); + rqlError.EndPosition.Should().Be(RqlSourcePosition.From(1, 10)); + rqlError.Rql.Should().Be(""); + } + + /// + /// Case 6 - RQL source is given and fails to parse, throwing a RqlException. + /// + [Fact] + public async Task ExecuteAsync_GivenRqlSourceCase6HavingErrorsOnParser_ThrowsRqlException() + { + // Arrange + var rql = "\"test string\";"; + var tokens = new[] + { + CreateToken("\"test string\"", "test string", TokenType.STRING), + CreateToken(";", null, TokenType.SEMICOLON), + }.ToList().AsReadOnly(); + var scanResult = ScanResult.CreateSuccess(tokens, new List()); + var messages = new List + { + Message.Create("Sample parse error", RqlSourcePosition.From(1, 1), RqlSourcePosition.From(1, 10), MessageSeverity.Error), + }; + var parseResult = ParseResult.CreateError(messages); + + Mock.Get(this.scanner) + .Setup(x => x.ScanTokens(It.Is(rql, StringComparer.Ordinal))) + .Returns(scanResult); + Mock.Get(this.parser) + .Setup(x => x.Parse(It.Is>(tks => object.Equals(tks, tokens)))) + .Returns(parseResult); + + // Act + var rqlException = await Assert.ThrowsAsync(async () => await this.rqlEngine.ExecuteAsync(rql)); + + // Assert + Mock.VerifyAll( + Mock.Get(this.scanner), + Mock.Get(this.parser)); + + rqlException.Message.Should().Be("Errors have occurred processing provided RQL source - Sample parse error for source @{1:1}-{1:10}"); + rqlException.Errors.Should().HaveCount(1); + var rqlError = rqlException.Errors.First(); + rqlError.Text.Should().Be("Sample parse error"); + rqlError.BeginPosition.Should().Be(RqlSourcePosition.From(1, 1)); + rqlError.EndPosition.Should().Be(RqlSourcePosition.From(1, 10)); + rqlError.Rql.Should().Be(""); + } + + /// + /// Case 7 - RQL source is given and fails to be interpreted, throwing a RqlException. + /// + [Fact] + public async Task ExecuteAsync_GivenRqlSourceCase7HavingErrorsOnInterpreter_ThrowsRqlException() + { + // Arrange + var rql = "\"test string\";"; + var tokens = new[] + { + CreateToken("\"test string\"", "test string", TokenType.STRING), + CreateToken(";", null, TokenType.SEMICOLON), + }.ToList().AsReadOnly(); + var scanResult = ScanResult.CreateSuccess(tokens, new List()); + var statements = new[] + { + ExpressionStatement.Create( + LiteralExpression.Create(LiteralType.Integer, tokens[0], tokens[0].Literal)), // "test string" + }.ToList().AsReadOnly(); + var parseResult = ParseResult.CreateSuccess(statements, new List()); + var interpretResult = new InterpretResult(); + interpretResult.AddStatementResult(new ErrorStatementResult("Sample interpret error", rql, RqlSourcePosition.From(1, 1), RqlSourcePosition.From(1, 10))); + + Mock.Get(this.scanner) + .Setup(x => x.ScanTokens(It.Is(rql, StringComparer.Ordinal))) + .Returns(scanResult); + Mock.Get(this.parser) + .Setup(x => x.Parse(It.Is>(tks => object.Equals(tks, tokens)))) + .Returns(parseResult); + Mock.Get(this.interpreter) + .Setup(x => x.InterpretAsync(It.Is>(stmts => object.Equals(stmts, statements)))) + .Returns(Task.FromResult(interpretResult)); + + // Act + var rqlException = await Assert.ThrowsAsync(async () => await this.rqlEngine.ExecuteAsync(rql)); + + // Assert + Mock.VerifyAll( + Mock.Get(this.scanner), + Mock.Get(this.parser), + Mock.Get(this.interpreter)); + + rqlException.Message.Should().Be("Errors have occurred processing provided RQL source - Sample interpret error for source \"test string\"; @{1:1}-{1:10}"); + rqlException.Errors.Should().HaveCount(1); + var rqlError = rqlException.Errors.First(); + rqlError.Text.Should().Be("Sample interpret error"); + rqlError.BeginPosition.Should().Be(RqlSourcePosition.From(1, 1)); + rqlError.EndPosition.Should().Be(RqlSourcePosition.From(1, 10)); + rqlError.Rql.Should().Be(rql); + } + + /// + /// Case 8 - RQL source is given which produces a unknown result type, throwing a NotSupportedException. + /// + [Fact] + public async Task ExecuteAsync_GivenRqlSourceCase8HavingUnknownResultType_ThrowsNotSupportedException() + { + // Arrange + var rql = "\"test string\";"; + var tokens = new[] + { + CreateToken("\"test string\"", "test string", TokenType.STRING), + CreateToken(";", null, TokenType.SEMICOLON), + }.ToList().AsReadOnly(); + var scanResult = ScanResult.CreateSuccess(tokens, new List()); + var statements = new[] + { + ExpressionStatement.Create( + LiteralExpression.Create(LiteralType.Integer, tokens[0], tokens[0].Literal)), // "test string" + }.ToList().AsReadOnly(); + var parseResult = ParseResult.CreateSuccess(statements, new List()); + var interpretResult = new InterpretResult(); + interpretResult.AddStatementResult(new StubResult()); + + Mock.Get(this.scanner) + .Setup(x => x.ScanTokens(It.Is(rql, StringComparer.Ordinal))) + .Returns(scanResult); + Mock.Get(this.parser) + .Setup(x => x.Parse(It.Is>(tks => object.Equals(tks, tokens)))) + .Returns(parseResult); + Mock.Get(this.interpreter) + .Setup(x => x.InterpretAsync(It.Is>(stmts => object.Equals(stmts, statements)))) + .Returns(Task.FromResult(interpretResult)); + + // Act + var notSupportedException = await Assert.ThrowsAsync(async () => await this.rqlEngine.ExecuteAsync(rql)); + + // Assert + Mock.VerifyAll( + Mock.Get(this.scanner), + Mock.Get(this.parser), + Mock.Get(this.interpreter)); + + notSupportedException.Message.Should().Be($"Result of type '{typeof(StubResult).FullName}' is not supported."); + } + + private static Token CreateToken(string lexeme, object? literal, TokenType type) + => Token.Create(lexeme, false, literal, RqlSourcePosition.Empty, RqlSourcePosition.Empty, (uint)lexeme.Length, type); + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Rules.Framework.Rql.Tests.csproj b/tests/Rules.Framework.Rql.Tests/Rules.Framework.Rql.Tests.csproj new file mode 100644 index 00000000..a21d2341 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Rules.Framework.Rql.Tests.csproj @@ -0,0 +1,31 @@ + + + + net6.0 + 10.0 + enable + enable + false + true + + + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + + diff --git a/tests/Rules.Framework.Rql.Tests/RulesEngineExtensionsTests.cs b/tests/Rules.Framework.Rql.Tests/RulesEngineExtensionsTests.cs new file mode 100644 index 00000000..2925d813 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/RulesEngineExtensionsTests.cs @@ -0,0 +1,49 @@ +namespace Rules.Framework.Rql.Tests +{ + using FluentAssertions; + using Moq; + using Rules.Framework.Rql.Tests.Stubs; + using Xunit; + + public class RulesEngineExtensionsTests + { + [Fact] + public void GetRqlEngine_GivenRulesEngine_BuildsRqlEngineWithDefaultRqlOptions() + { + // Arrange + var rulesEngine = Mock.Of>(); + + // Act + var rqlEngine = rulesEngine.GetRqlEngine(); + + // Assert + rqlEngine.Should().NotBeNull(); + } + + [Fact] + public void GetRqlEngine_GivenRulesEngineWithNonEnumConditionType_ThrowsNotSupportedException() + { + // Arrange + var rulesEngine = Mock.Of>(); + + // Act + var notSupportedException = Assert.Throws(() => rulesEngine.GetRqlEngine()); + + // Assert + notSupportedException.Message.Should().Be("Rule Query Language is not supported for non-enum types of TConditionType."); + } + + [Fact] + public void GetRqlEngine_GivenRulesEngineWithNonEnumContentType_ThrowsNotSupportedException() + { + // Arrange + var rulesEngine = Mock.Of>(); + + // Act + var notSupportedException = Assert.Throws(() => rulesEngine.GetRqlEngine()); + + // Assert + notSupportedException.Message.Should().Be("Rule Query Language is not supported for non-enum types of TContentType."); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Runtime/RqlRuntimeTests.cs b/tests/Rules.Framework.Rql.Tests/Runtime/RqlRuntimeTests.cs new file mode 100644 index 00000000..e3c05f98 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/Runtime/RqlRuntimeTests.cs @@ -0,0 +1,351 @@ +namespace Rules.Framework.Rql.Tests.Runtime +{ + using FluentAssertions; + using Moq; + using Rules.Framework.Core; + using Rules.Framework.Rql.Runtime; + using Rules.Framework.Rql.Runtime.RuleManipulation; + using Rules.Framework.Rql.Runtime.Types; + using Rules.Framework.Rql.Tests.Stubs; + using Xunit; + + public class RqlRuntimeTests + { + public static IEnumerable ApplyBinary_ErrorCases() => new[] + { + // RqlOperators.Minus + new object?[] { new RqlInteger(1), RqlOperators.Minus, new RqlDecimal(2.0m), "Expected right operand of type integer but found decimal." }, + new object?[] { new RqlDecimal(1.5m), RqlOperators.Minus, new RqlInteger(3), "Expected right operand of type decimal but found integer." }, + new object?[] { new RqlDecimal(1.5m), RqlOperators.Minus, new RqlBool(true), "Expected right operand of type decimal but found bool." }, + new object?[] { new RqlInteger(9), RqlOperators.Minus, new RqlBool(true), "Expected right operand of type integer but found bool." }, + new object?[] { new RqlString("abc"), RqlOperators.Minus, new RqlInteger(1), "Cannot subtract operand of type string." }, + + // RqlOperators.Plus + new object?[] { new RqlInteger(1), RqlOperators.Plus, new RqlDecimal(2.0m), "Expected right operand of type integer but found decimal." }, + new object?[] { new RqlDecimal(1.5m), RqlOperators.Plus, new RqlInteger(3), "Expected right operand of type decimal but found integer." }, + new object?[] { new RqlDecimal(1.5m), RqlOperators.Plus, new RqlBool(true), "Expected right operand of type decimal but found bool." }, + new object?[] { new RqlInteger(9), RqlOperators.Plus, new RqlBool(true), "Expected right operand of type integer but found bool." }, + new object?[] { new RqlString("abc"), RqlOperators.Plus, new RqlInteger(1), "Cannot sum operand of type string." }, + + // RqlOperators.Slash + new object?[] { new RqlInteger(1), RqlOperators.Slash, new RqlDecimal(2.0m), "Expected right operand of type integer but found decimal." }, + new object?[] { new RqlDecimal(1.5m), RqlOperators.Slash, new RqlInteger(3), "Expected right operand of type decimal but found integer." }, + new object?[] { new RqlDecimal(1.5m), RqlOperators.Slash, new RqlBool(true), "Expected right operand of type decimal but found bool." }, + new object?[] { new RqlInteger(9), RqlOperators.Slash, new RqlBool(true), "Expected right operand of type integer but found bool." }, + new object?[] { new RqlString("abc"), RqlOperators.Slash, new RqlInteger(1), "Cannot divide operand of type string." }, + + // RqlOperators.Star + new object?[] { new RqlInteger(1), RqlOperators.Star, new RqlDecimal(2.0m), "Expected right operand of type integer but found decimal." }, + new object?[] { new RqlDecimal(1.5m), RqlOperators.Star, new RqlInteger(3), "Expected right operand of type decimal but found integer." }, + new object?[] { new RqlDecimal(1.5m), RqlOperators.Star, new RqlBool(true), "Expected right operand of type decimal but found bool." }, + new object?[] { new RqlInteger(9), RqlOperators.Star, new RqlBool(true), "Expected right operand of type integer but found bool." }, + new object?[] { new RqlString("abc"), RqlOperators.Star, new RqlInteger(1), "Cannot multiply operand of type string." }, + }; + + public static IEnumerable ApplyBinary_SuccessCases() => new[] + { + // RqlOperators.Minus + new object?[] { new RqlInteger(5), RqlOperators.Minus, new RqlInteger(4), new RqlInteger(1) }, + new object?[] { new RqlDecimal(5.1m), RqlOperators.Minus, new RqlDecimal(2.3m), new RqlDecimal(2.8m) }, + new object?[] { new RqlAny(new RqlInteger(5)), RqlOperators.Minus, new RqlInteger(4), new RqlInteger(1) }, + new object?[] { new RqlDecimal(5.1m), RqlOperators.Minus, new RqlAny(new RqlDecimal(2.3m)), new RqlDecimal(2.8m) }, + + // RqlOperators.Plus + new object?[] { new RqlInteger(2), RqlOperators.Plus, new RqlInteger(4), new RqlInteger(6) }, + new object?[] { new RqlDecimal(5.1m), RqlOperators.Plus, new RqlDecimal(14.3m), new RqlDecimal(19.4m) }, + new object?[] { new RqlAny(new RqlInteger(2)), RqlOperators.Plus, new RqlInteger(4), new RqlInteger(6) }, + new object?[] { new RqlDecimal(5.1m), RqlOperators.Plus, new RqlAny(new RqlDecimal(14.3m)), new RqlDecimal(19.4m) }, + + // RqlOperators.Slash + new object?[] { new RqlInteger(6), RqlOperators.Slash, new RqlInteger(2), new RqlInteger(3) }, + new object?[] { new RqlDecimal(5.1m), RqlOperators.Slash, new RqlDecimal(2m), new RqlDecimal(2.55m) }, + new object?[] { new RqlAny(new RqlInteger(6)), RqlOperators.Slash, new RqlInteger(2), new RqlInteger(3) }, + new object?[] { new RqlDecimal(5.1m), RqlOperators.Slash, new RqlAny(new RqlDecimal(2m)), new RqlDecimal(2.55m) }, + + // RqlOperators.Star + new object?[] { new RqlInteger(6), RqlOperators.Star, new RqlInteger(2), new RqlInteger(12) }, + new object?[] { new RqlDecimal(5.1m), RqlOperators.Star, new RqlDecimal(2m), new RqlDecimal(10.2m) }, + new object?[] { new RqlAny(new RqlInteger(6)), RqlOperators.Star, new RqlInteger(2), new RqlInteger(12) }, + new object?[] { new RqlDecimal(5.1m), RqlOperators.Star, new RqlAny(new RqlDecimal(2m)), new RqlDecimal(10.2m) }, + new object?[] { new RqlInteger(1), RqlOperators.None, new RqlInteger(1), new RqlNothing() }, + }; + + public static IEnumerable ApplyUnary_ErrorCases() => new[] + { + new object?[] { new RqlInteger(10), RqlOperators.Plus, "Unary operator Plus is not supported for value ' 10'." }, + new object?[] { new RqlString("abc"), RqlOperators.Minus, "Unary operator Minus is not supported for value ' \"abc\"'." }, + }; + + public static IEnumerable ApplyUnary_SuccessCases() => new[] + { + new object?[] { new RqlInteger(10), RqlOperators.Minus, new RqlInteger(-10) }, + new object?[] { new RqlDecimal(34.7m), RqlOperators.Minus, new RqlDecimal(-34.7m) }, + new object?[] { new RqlAny(new RqlInteger(10)), RqlOperators.Minus, new RqlInteger(-10) }, + new object?[] { new RqlAny(new RqlDecimal(34.7m)), RqlOperators.Minus, new RqlDecimal(-34.7m) }, + }; + + [Theory] + [MemberData(nameof(ApplyBinary_ErrorCases))] + public void ApplyBinary_ErrorConditions_ThrowsRuntimeException(object left, object @operator, object right, string expectedErrorMessage) + { + // Arrange + var rulesEngine = Mock.Of>(); + var rqlRuntime = RqlRuntime.Create(rulesEngine); + + // Act + var runtimeException = Assert.Throws(() => rqlRuntime.ApplyBinary((IRuntimeValue)left, (RqlOperators)@operator, (IRuntimeValue)right)); + + // Assert + runtimeException.Message.Should().Be(expectedErrorMessage); + } + + [Theory] + [MemberData(nameof(ApplyBinary_SuccessCases))] + public void ApplyBinary_SuccessConditions_ReturnsBinaryResult(object left, object @operator, object right, object expected) + { + // Arrange + var rulesEngine = Mock.Of>(); + var rqlRuntime = RqlRuntime.Create(rulesEngine); + + // Act + var actual = rqlRuntime.ApplyBinary((IRuntimeValue)left, (RqlOperators)@operator, (IRuntimeValue)right); + + // Assert + actual.Should().BeEquivalentTo(expected); + } + + [Theory] + [MemberData(nameof(ApplyUnary_ErrorCases))] + public void ApplyUnary_ErrorConditions_ThrowsRuntimeException(object operand, object @operator, string expectedErrorMessage) + { + // Arrange + var rulesEngine = Mock.Of>(); + var rqlRuntime = RqlRuntime.Create(rulesEngine); + + // Act + var runtimeException = Assert.Throws(() => rqlRuntime.ApplyUnary((IRuntimeValue)operand, (RqlOperators)@operator)); + + // Assert + runtimeException.Message.Should().Be(expectedErrorMessage); + } + + [Theory] + [MemberData(nameof(ApplyUnary_SuccessCases))] + public void ApplyUnary_SuccessConditions_ReturnsUnaryResult(object operand, object @operator, object expected) + { + // Arrange + var rulesEngine = Mock.Of>(); + var rqlRuntime = RqlRuntime.Create(rulesEngine); + + // Act + var actual = rqlRuntime.ApplyUnary((IRuntimeValue)operand, (RqlOperators)@operator); + + // Assert + actual.Should().BeEquivalentTo(expected); + } + + [Fact] + public void Create_GivenRulesEngine_ReturnsNewRqlRuntime() + { + // Arrange + var rulesEngine = Mock.Of>(); + + // Act + var rqlRuntime = RqlRuntime.Create(rulesEngine); + + // Assert + rqlRuntime.Should().NotBeNull(); + } + + [Fact] + public async Task MatchRulesAsync_GivenAllMatchCardinalityWithResult_ReturnsRqlArrayWithTwoRules() + { + // Arrange + const MatchCardinality matchCardinality = MatchCardinality.All; + const ContentType contentType = ContentType.Type1; + var matchDate = new RqlDate(DateTime.Parse("2024-04-13Z")); + var conditions = new[] + { + new Condition(ConditionType.IsoCountryCode, "PT") + }; + var matchRulesArgs = new MatchRulesArgs + { + Conditions = conditions, + ContentType = contentType, + MatchCardinality = matchCardinality, + MatchDate = matchDate, + }; + + var expectedRule1 = BuildRule("Rule 1", DateTime.Parse("2024-01-01Z"), DateTime.Parse("2025-01-01Z"), new object(), contentType); + var expectedRule2 = BuildRule("Rule 2", DateTime.Parse("2024-01-01Z"), DateTime.Parse("2025-01-01Z"), new object(), contentType); + var expectedRules = new[] { expectedRule1, expectedRule2 }; + var rulesEngine = Mock.Of>(); + Mock.Get(rulesEngine) + .Setup(x => x.MatchManyAsync(contentType, matchDate.Value, It.Is>>(c => c.SequenceEqual(conditions)))) + .ReturnsAsync(expectedRules); + var rqlRuntime = RqlRuntime.Create(rulesEngine); + + // Act + var actual = await rqlRuntime.MatchRulesAsync(matchRulesArgs); + + // Assert + actual.Should().NotBeNull(); + actual.Size.Value.Should().Be(2); + actual.Value[0].Unwrap().Should().BeOfType>() + .Subject.Value.Should().BeSameAs(expectedRule1); + actual.Value[1].Unwrap().Should().BeOfType>() + .Subject.Value.Should().BeSameAs(expectedRule2); + } + + [Fact] + public async Task MatchRulesAsync_GivenNoneMatchCardinality_ThrowsArgumentException() + { + // Arrange + const MatchCardinality matchCardinality = MatchCardinality.None; + const ContentType contentType = ContentType.Type1; + var matchDate = new RqlDate(DateTime.Parse("2024-04-13Z")); + var conditions = Array.Empty>(); + var matchRulesArgs = new MatchRulesArgs + { + Conditions = conditions, + ContentType = contentType, + MatchCardinality = matchCardinality, + MatchDate = matchDate, + }; + + var rulesEngine = Mock.Of>(); + var rqlRuntime = RqlRuntime.Create(rulesEngine); + + // Act + var actual = await Assert.ThrowsAsync(async () => await rqlRuntime.MatchRulesAsync(matchRulesArgs)); + + // Assert + actual.Should().NotBeNull(); + actual.ParamName.Should().Be(nameof(matchRulesArgs)); + actual.Message.Should().StartWith("A valid match cardinality must be provided."); + } + + [Fact] + public async Task MatchRulesAsync_GivenOneMatchCardinalityWithoutResult_ReturnsEmptyRqlArray() + { + // Arrange + const MatchCardinality matchCardinality = MatchCardinality.One; + const ContentType contentType = ContentType.Type1; + var matchDate = new RqlDate(DateTime.Parse("2024-04-13Z")); + var conditions = new[] + { + new Condition(ConditionType.IsoCountryCode, "PT") + }; + var matchRulesArgs = new MatchRulesArgs + { + Conditions = conditions, + ContentType = contentType, + MatchCardinality = matchCardinality, + MatchDate = matchDate, + }; + + var expectedRule = BuildRule("Rule 1", DateTime.Parse("2024-01-01Z"), DateTime.Parse("2025-01-01Z"), new object(), contentType); + var rulesEngine = Mock.Of>(); + Mock.Get(rulesEngine) + .Setup(x => x.MatchOneAsync(contentType, matchDate.Value, It.Is>>(c => c.SequenceEqual(conditions)))) + .Returns(Task.FromResult>(null!)); + var rqlRuntime = RqlRuntime.Create(rulesEngine); + + // Act + var actual = await rqlRuntime.MatchRulesAsync(matchRulesArgs); + + // Assert + actual.Should().NotBeNull(); + actual.Size.Value.Should().Be(0); + } + + [Fact] + public async Task MatchRulesAsync_GivenOneMatchCardinalityWithResult_ReturnsRqlArrayWithOneRule() + { + // Arrange + const MatchCardinality matchCardinality = MatchCardinality.One; + const ContentType contentType = ContentType.Type1; + var matchDate = new RqlDate(DateTime.Parse("2024-04-13Z")); + var conditions = new[] + { + new Condition(ConditionType.IsoCountryCode, "PT") + }; + var matchRulesArgs = new MatchRulesArgs + { + Conditions = conditions, + ContentType = contentType, + MatchCardinality = matchCardinality, + MatchDate = matchDate, + }; + + var expectedRule = BuildRule("Rule 1", DateTime.Parse("2024-01-01Z"), DateTime.Parse("2025-01-01Z"), new object(), contentType); + var rulesEngine = Mock.Of>(); + Mock.Get(rulesEngine) + .Setup(x => x.MatchOneAsync(contentType, matchDate.Value, It.Is>>(c => c.SequenceEqual(conditions)))) + .ReturnsAsync(expectedRule); + var rqlRuntime = RqlRuntime.Create(rulesEngine); + + // Act + var actual = await rqlRuntime.MatchRulesAsync(matchRulesArgs); + + // Assert + actual.Should().NotBeNull(); + actual.Size.Value.Should().Be(1); + actual.Value[0].Unwrap().Should().BeOfType>() + .Subject.Value.Should().BeSameAs(expectedRule); + } + + [Fact] + public async Task MatchSearchRulesAsync_GivenSearchArgs_ReturnsRqlArrayWithTwoRules() + { + // Arrange + const ContentType contentType = ContentType.Type1; + var dateBegin = new RqlDate(DateTime.Parse("2020-01-01Z")); + var dateEnd = new RqlDate(DateTime.Parse("2030-01-01Z")); + var conditions = new[] + { + new Condition(ConditionType.IsoCountryCode, "PT") + }; + var searchRulesArgs = new SearchRulesArgs + { + Conditions = conditions, + ContentType = contentType, + DateBegin = dateBegin, + DateEnd = dateEnd, + }; + + var expectedRule1 = BuildRule("Rule 1", DateTime.Parse("2024-01-01Z"), DateTime.Parse("2025-01-01Z"), new object(), contentType); + var expectedRule2 = BuildRule("Rule 2", DateTime.Parse("2024-01-01Z"), DateTime.Parse("2025-01-01Z"), new object(), contentType); + var expectedRules = new[] { expectedRule1, expectedRule2 }; + var rulesEngine = Mock.Of>(); + Mock.Get(rulesEngine) + .Setup(x => x.SearchAsync(It.Is>(c => c.ExcludeRulesWithoutSearchConditions == true + && c.Conditions.Equals(searchRulesArgs.Conditions) + && c.ContentType.Equals(searchRulesArgs.ContentType) + && c.DateBegin.Equals(searchRulesArgs.DateBegin.Value) + && c.DateEnd.Equals(searchRulesArgs.DateEnd.Value)))) + .ReturnsAsync(expectedRules); + var rqlRuntime = RqlRuntime.Create(rulesEngine); + + // Act + var actual = await rqlRuntime.SearchRulesAsync(searchRulesArgs); + + // Assert + actual.Should().NotBeNull(); + actual.Size.Value.Should().Be(2); + actual.Value[0].Unwrap().Should().BeOfType>() + .Subject.Value.Should().BeSameAs(expectedRule1); + actual.Value[1].Unwrap().Should().BeOfType>() + .Subject.Value.Should().BeSameAs(expectedRule2); + } + + private static Rule BuildRule(string name, DateTime dateBegin, DateTime? dateEnd, object content, ContentType contentType) + { + return RuleBuilder.NewRule() + .WithName(name) + .WithDatesInterval(dateBegin, dateEnd.GetValueOrDefault()) + .WithContent(contentType, content) + .Build().Rule; + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/TestStubs/ConditionType.cs b/tests/Rules.Framework.Rql.Tests/TestStubs/ConditionType.cs new file mode 100644 index 00000000..55a1a08b --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/TestStubs/ConditionType.cs @@ -0,0 +1,15 @@ +namespace Rules.Framework.Rql.Tests.Stubs +{ + internal enum ConditionType + { + IsoCountryCode = 1, + + IsoCurrency = 2, + + NumberOfSales = 3, + + PluviosityRate = 4, + + IsVip = 5 + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/TestStubs/ContentType.cs b/tests/Rules.Framework.Rql.Tests/TestStubs/ContentType.cs new file mode 100644 index 00000000..6bb08462 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/TestStubs/ContentType.cs @@ -0,0 +1,9 @@ +namespace Rules.Framework.Rql.Tests.Stubs +{ + internal enum ContentType + { + Type1 = 1, + + Type2 = 2 + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/TestStubs/StubResult.cs b/tests/Rules.Framework.Rql.Tests/TestStubs/StubResult.cs new file mode 100644 index 00000000..42a49ef5 --- /dev/null +++ b/tests/Rules.Framework.Rql.Tests/TestStubs/StubResult.cs @@ -0,0 +1,14 @@ +namespace Rules.Framework.Rql.Tests.TestStubs +{ + using System; + using Rules.Framework.Rql.Pipeline.Interpret; + + internal class StubResult : IResult + { + public bool HasOutput => throw new NotImplementedException(); + + public string Rql => throw new NotImplementedException(); + + public bool Success => true; + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.RqlReplTester/Program.cs b/tests/Rules.Framework.RqlReplTester/Program.cs new file mode 100644 index 00000000..36565438 --- /dev/null +++ b/tests/Rules.Framework.RqlReplTester/Program.cs @@ -0,0 +1,195 @@ +namespace Rules.Framework.RqlReplTester +{ + using System.Text; + using McMaster.Extensions.CommandLineUtils; + using Newtonsoft.Json; + using Rules.Framework.IntegrationTests.Common.Scenarios; + using Rules.Framework.IntegrationTests.Common.Scenarios.Scenario8; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Runtime.Types; + + internal class Program + { + private static readonly ConsoleColor originalConsoleForegroundColor = Console.ForegroundColor; + private static readonly string tab = new string(' ', 4); + + private static async Task ExecuteAsync(IRqlEngine rqlEngine, string? input) + { + try + { + var results = await rqlEngine.ExecuteAsync(input); + foreach (var result in results) + { + Console.ForegroundColor = originalConsoleForegroundColor; + switch (result) + { + case RulesSetResult rulesResultSet: + HandleRulesSetResult(rulesResultSet); + break; + + case NothingResult: + // Nothing to be done. + break; + + case ValueResult valueResult: + HandleObjectResult(valueResult); + break; + + default: + throw new NotSupportedException($"Result type is not supported: '{result.GetType().FullName}'"); + } + } + } + catch (RqlException rqlException) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"{rqlException.Message} Errors:"); + + foreach (var rqlError in rqlException.Errors) + { + var errorMessageBuilder = new StringBuilder(" - ") + .Append(rqlError.Text) + .Append(" @") + .Append(rqlError.BeginPosition) + .Append('-') + .Append(rqlError.EndPosition); + Console.WriteLine(errorMessageBuilder.ToString()); + } + + Console.ForegroundColor = ConsoleColor.Gray; + } + + Console.WriteLine(); + } + + private static void HandleObjectResult(ValueResult result) + { + Console.WriteLine(); + var rawValue = result.Value switch + { + RqlAny rqlAny when rqlAny.UnderlyingType == RqlTypes.Object => rqlAny.ToString() ?? string.Empty, + RqlAny rqlAny => rqlAny.ToString() ?? string.Empty, + _ => result.Value.ToString(), + }; + var value = rawValue!.Replace("\n", $"\n{tab}"); + Console.WriteLine($"{tab}{value}"); + } + + private static void HandleRulesSetResult(RulesSetResult result) + { + Console.WriteLine(); + if (result.Lines.Any()) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine($"{tab}{result.Rql}"); + Console.ForegroundColor = originalConsoleForegroundColor; + Console.WriteLine($"{tab}{new string('-', Math.Min(result.Rql.Length, Console.WindowWidth - 5))}"); + Console.ForegroundColor = ConsoleColor.Green; + if (result.NumberOfRules > 0) + { + Console.WriteLine($"{tab} {result.NumberOfRules} rules were returned."); + } + else + { + Console.WriteLine($"{tab} {result.Lines.Count} rules were returned."); + } + + Console.ForegroundColor = originalConsoleForegroundColor; + Console.WriteLine(); + Console.WriteLine($"{tab} | # | Priority | Status | Range | Rule"); + Console.WriteLine($"{tab}{new string('-', Console.WindowWidth - 5)}"); + + foreach (var line in result.Lines) + { + var rule = line.Rule.Value; + var lineNumber = line.LineNumber.ToString(); + var priority = rule.Priority.ToString(); + var active = rule.Active ? "Active" : "Inactive"; + var dateBegin = rule.DateBegin.Date.ToString("yyyy-MM-ddZ"); + var dateEnd = rule.DateEnd?.Date.ToString("yyyy-MM-ddZ") ?? "(no end)"; + var ruleName = rule.Name; + var content = JsonConvert.SerializeObject(rule.ContentContainer.GetContentAs()); + + Console.WriteLine($"{tab} | {lineNumber} | {priority,-8} | {active,-8} | {dateBegin,-11} - {dateEnd,-11} | {ruleName}: {content}"); + } + } + else if (result.NumberOfRules > 0) + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine($"{tab}{result.Rql}"); + Console.ForegroundColor = originalConsoleForegroundColor; + Console.WriteLine($"{tab}{new string('-', result.Rql.Length)}"); + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"{tab} {result.NumberOfRules} rules were affected."); + Console.ForegroundColor = originalConsoleForegroundColor; + } + else + { + Console.ForegroundColor = ConsoleColor.DarkGray; + Console.WriteLine($"{tab}{result.Rql}"); + Console.ForegroundColor = originalConsoleForegroundColor; + Console.WriteLine($"{tab}{new string('-', result.Rql.Length)}"); + Console.WriteLine($"{tab} (empty)"); + } + } + + private static async Task Main(string[] args) + { + var app = new CommandLineApplication(); + + app.HelpOption(); + + var artifactsPathOption = app.Option("-s|--script ", "Sets a script to be executed", CommandOptionType.SingleValue, config => + { + config.DefaultValue = null; + }); + + app.OnExecuteAsync(async (ct) => + { + var rulesEngine = RulesEngineBuilder.CreateRulesEngine() + .WithContentType() + .WithConditionType() + .SetInMemoryDataSource() + .Build(); + + await ScenarioLoader.LoadScenarioAsync(rulesEngine, new Scenario8Data()); + var rqlEngine = rulesEngine.GetRqlEngine(); + + var script = artifactsPathOption.Value(); + if (string.IsNullOrEmpty(script)) + { + while (true) + { + Console.Write("> "); + var input = Console.ReadLine(); + if (string.IsNullOrEmpty(input)) + { + continue; + } + + if (input.ToUpperInvariant() == "EXIT") + { + break; + } + + await ExecuteAsync(rqlEngine, input); + } + } + else + { + var directory = Directory.GetParent(Environment.CurrentDirectory); + while (!string.Equals(directory!.Name, "bin")) + { + directory = directory.Parent; + } + + var scriptFullPath = Path.Combine(directory.Parent!.FullName, script); + var scriptContent = await File.ReadAllTextAsync(scriptFullPath, ct).ConfigureAwait(false); + await ExecuteAsync(rqlEngine, scriptContent); + } + }); + + return await app.ExecuteAsync(args).ConfigureAwait(false); + } + } +} \ No newline at end of file diff --git a/tests/Rules.Framework.RqlReplTester/Rules.Framework.RqlReplTester.csproj b/tests/Rules.Framework.RqlReplTester/Rules.Framework.RqlReplTester.csproj new file mode 100644 index 00000000..f30b7faf --- /dev/null +++ b/tests/Rules.Framework.RqlReplTester/Rules.Framework.RqlReplTester.csproj @@ -0,0 +1,26 @@ + + + + Exe + net6.0 + enable + enable + + + + + Never + + + + + + + + + + + + + + diff --git a/tests/Rules.Framework.RqlReplTester/test-script.rql b/tests/Rules.Framework.RqlReplTester/test-script.rql new file mode 100644 index 00000000..6eb7702f --- /dev/null +++ b/tests/Rules.Framework.RqlReplTester/test-script.rql @@ -0,0 +1,49 @@ +var #rules = match one rule for "TexasHoldemPokerSingleCombinations" on $2023-01-01Z$ with { @NumberOfKings is 1, @NumberOfQueens is 1, @NumberOfJacks is 1, @NumberOfTens is 1, @NumberOfNines is 1, @KingOfClubs is true, @QueenOfDiamonds is true, @JackOfClubs is true, @TenOfHearts is true, @NineOfSpades is true }; + +{ + Show(#rules); + Show(#rules.Empty()); + + Show(#rules.NotEmpty()); +} + +{ + +} + +if (#rules.NotEmpty()) +{ + Show("Rules are not empty!"); + Show(#rules[0]); +} + +if (#rules.Empty()) +{ + Show("Rules are empty!"); +} +else +{ + Show("Found rules for you!"); +} + +var arr = { 1, 2, 3 }; +if (arr.Empty()) + Show("Array is empty."); +else + Show(arr); + +foreach (var #rule in #rules) +{ + Show(#rule.#Name); + if (#rule.#Priority >= 50) + { + Show("Big priority but not that big."); + } +} + +foreach (var num in arr) +{ + Show(num); + var r = match one rule for "TexasHoldemPokerSingleCombinations" on $2023-01-01Z$ with { @NumberOfKings is num, @NumberOfQueens is 1, @NumberOfJacks is 1, @NumberOfTens is 1, @NumberOfNines is 1, @KingOfClubs is true, @QueenOfDiamonds is true, @JackOfClubs is true, @TenOfHearts is true, @NineOfSpades is true }; + Show(r); +} \ No newline at end of file diff --git a/tests/Rules.Framework.Tests/Rules.Framework.Tests.csproj b/tests/Rules.Framework.Tests/Rules.Framework.Tests.csproj index b35bf875..bf06cb6e 100644 --- a/tests/Rules.Framework.Tests/Rules.Framework.Tests.csproj +++ b/tests/Rules.Framework.Tests/Rules.Framework.Tests.csproj @@ -2,17 +2,10 @@ net8.0 - 9.0 + 10.0 Full false - - - - 10.0 - - - - 10.0 + true From 7ea0c57842ca29c7373ec7d1ff81c69a2cd2f777 Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Sun, 19 May 2024 15:35:34 +0100 Subject: [PATCH 2/9] feat: add RQL support to Web UI --- .../Pipeline/Interpret/Interpreter.cs | 62 ++-- ...Extensions.cs => RulesEngineExtensions.cs} | 18 +- src/Rules.Framework.WebUI/Dto/RqlErrorDto.cs | 15 + src/Rules.Framework.WebUI/Dto/RqlInputDto.cs | 7 + src/Rules.Framework.WebUI/Dto/RqlOutputDto.cs | 11 + .../Dto/RqlRuntimeValueDto.cs | 11 + .../Dto/RqlStatementOutputDto.cs | 17 ++ src/Rules.Framework.WebUI/Dto/RqlTypeDto.cs | 7 + .../Extensions/RqlOutputDtoExtensions.cs | 80 +++++ .../Extensions/RuleDtoExtensions.cs | 19 +- .../Handlers/GetContentTypeHandler.cs | 21 +- .../Handlers/GetRulesHandler.cs | 12 +- .../Handlers/PostRqlHandler.cs | 61 ++++ .../Rules.Framework.WebUI.csproj | 1 + .../WebUIApplicationBuilderExtensions.cs | 3 +- src/Rules.Framework.WebUI/index.html | 275 +++++++++++++++++- .../Extensions/GenericRuleExtensions.cs | 57 ---- .../Extensions/RuleExtensions.cs | 106 +++++++ .../Extensions/RulesEngineExtensions.cs | 6 + ...sExtensions.cs => SearchArgsExtensions.cs} | 12 +- .../Generics/GenericComposedConditionNode.cs | 16 - .../Generics/GenericConditionNode.cs | 15 - .../Generics/GenericConditionType.cs | 31 -- .../Generics/GenericContentType.cs | 31 -- src/Rules.Framework/Generics/GenericRule.cs | 45 --- .../Generics/GenericRulesEngine.cs | 65 ++++- .../Generics/GenericValueConditionNode.cs | 30 -- .../Generics/IGenericRulesEngine.cs | 17 +- .../InterpreterTests.InputConditionSegment.cs | 3 +- .../RulesEngineExtensionsTests.cs | 23 +- ...ensionsTests.cs => RuleExtensionsTests.cs} | 70 ++--- ...sTests.cs => SearchArgsExtensionsTests.cs} | 30 +- .../Generics/GenericRulesEngineTests.cs | 30 +- .../Extensions/RuleDtoExtensionsTests.cs | 14 +- .../Handlers/GetRulesHandlerTests.cs | 16 +- 35 files changed, 804 insertions(+), 433 deletions(-) rename src/Rules.Framework.Rql/{RuleEngineExtensions.cs => RulesEngineExtensions.cs} (60%) create mode 100644 src/Rules.Framework.WebUI/Dto/RqlErrorDto.cs create mode 100644 src/Rules.Framework.WebUI/Dto/RqlInputDto.cs create mode 100644 src/Rules.Framework.WebUI/Dto/RqlOutputDto.cs create mode 100644 src/Rules.Framework.WebUI/Dto/RqlRuntimeValueDto.cs create mode 100644 src/Rules.Framework.WebUI/Dto/RqlStatementOutputDto.cs create mode 100644 src/Rules.Framework.WebUI/Dto/RqlTypeDto.cs create mode 100644 src/Rules.Framework.WebUI/Extensions/RqlOutputDtoExtensions.cs create mode 100644 src/Rules.Framework.WebUI/Handlers/PostRqlHandler.cs delete mode 100644 src/Rules.Framework/Extensions/GenericRuleExtensions.cs create mode 100644 src/Rules.Framework/Extensions/RuleExtensions.cs rename src/Rules.Framework/Extensions/{GenericSearchArgsExtensions.cs => SearchArgsExtensions.cs} (84%) delete mode 100644 src/Rules.Framework/Generics/GenericComposedConditionNode.cs delete mode 100644 src/Rules.Framework/Generics/GenericConditionNode.cs delete mode 100644 src/Rules.Framework/Generics/GenericConditionType.cs delete mode 100644 src/Rules.Framework/Generics/GenericContentType.cs delete mode 100644 src/Rules.Framework/Generics/GenericRule.cs delete mode 100644 src/Rules.Framework/Generics/GenericValueConditionNode.cs rename tests/Rules.Framework.Tests/Extensions/{GenericRuleExtensionsTests.cs => RuleExtensionsTests.cs} (59%) rename tests/Rules.Framework.Tests/Extensions/{GenericSearchArgsExtensionsTests.cs => SearchArgsExtensionsTests.cs} (60%) diff --git a/src/Rules.Framework.Rql/Pipeline/Interpret/Interpreter.cs b/src/Rules.Framework.Rql/Pipeline/Interpret/Interpreter.cs index 62ae84d6..53958816 100644 --- a/src/Rules.Framework.Rql/Pipeline/Interpret/Interpreter.cs +++ b/src/Rules.Framework.Rql/Pipeline/Interpret/Interpreter.cs @@ -82,27 +82,9 @@ public Task VisitIdentifierExpression(IdentifierExpression identi public async Task VisitInputConditionSegment(InputConditionSegment inputConditionExpression) { - var conditionTypeName = (RqlString)await inputConditionExpression.Left.Accept(this).ConfigureAwait(false); - object conditionType; - -#if NETSTANDARD2_0 - try - { - conditionType = Enum.Parse(typeof(TConditionType), conditionTypeName.Value); - } - catch (Exception) - { - throw CreateInterpreterException(new[] { FormattableString.Invariant($"Condition type of name '{conditionTypeName}' was not found.") }, inputConditionExpression); - } -#else - if (!Enum.TryParse(typeof(TConditionType), conditionTypeName.Value, out conditionType)) - { - throw CreateInterpreterException(new[] { FormattableString.Invariant($"Condition type of name '{conditionTypeName}' was not found.") }, inputConditionExpression); - } -#endif - + var conditionType = await this.HandleConditionTypeAsync(inputConditionExpression.Left).ConfigureAwait(false); var conditionValue = await inputConditionExpression.Right.Accept(this).ConfigureAwait(false); - return new Condition((TConditionType)conditionType, conditionValue.RuntimeValue); + return new Condition(conditionType, conditionValue.RuntimeValue); } public async Task VisitInputConditionsSegment(InputConditionsSegment inputConditionsExpression) @@ -349,6 +331,38 @@ private Exception CreateInterpreterException(string error, IAstElement astElemen return CreateInterpreterException(new[] { error }, astElement); } + private async Task HandleConditionTypeAsync(Expression conditionTypeExpression) + { + var conditionTypeName = (RqlString)await conditionTypeExpression.Accept(this).ConfigureAwait(false); + object conditionType; + var type = typeof(TConditionType); + + if (type == typeof(string)) + { + conditionType = conditionTypeName.Value; + } + else + { +#if NETSTANDARD2_0 + try + { + conditionType = Enum.Parse(type, conditionTypeName.Value); + } + catch (Exception) + { + throw CreateInterpreterException(new[] { FormattableString.Invariant($"Condition type of name '{conditionTypeName}' was not found.") }, conditionTypeExpression); + } +#else + if (!Enum.TryParse(type, conditionTypeName.Value, out conditionType)) + { + throw CreateInterpreterException(new[] { FormattableString.Invariant($"Condition type of name '{conditionTypeName}' was not found.") }, conditionTypeExpression); + } +#endif + } + + return (TConditionType)conditionType; + } + private async Task HandleContentTypeAsync(Expression contentTypeExpression) { var rawValue = await contentTypeExpression.Accept(this).ConfigureAwait(false); @@ -360,7 +374,13 @@ private async Task HandleContentTypeAsync(Expression contentTypeEx try { - return (TContentType)Enum.Parse(typeof(TContentType), ((RqlString)value).Value, ignoreCase: true); + var type = typeof(TContentType); + if (type == typeof(string)) + { + return (TContentType)((RqlString)value).RuntimeValue; + } + + return (TContentType)Enum.Parse(type, ((RqlString)value).Value, ignoreCase: true); } catch (Exception) { diff --git a/src/Rules.Framework.Rql/RuleEngineExtensions.cs b/src/Rules.Framework.Rql/RulesEngineExtensions.cs similarity index 60% rename from src/Rules.Framework.Rql/RuleEngineExtensions.cs rename to src/Rules.Framework.Rql/RulesEngineExtensions.cs index 3bcb9d04..84571f5f 100644 --- a/src/Rules.Framework.Rql/RuleEngineExtensions.cs +++ b/src/Rules.Framework.Rql/RulesEngineExtensions.cs @@ -1,8 +1,9 @@ -namespace Rules.Framework.Rql +namespace Rules.Framework { using System; + using Rules.Framework.Rql; - public static class RuleEngineExtensions + public static class RulesEngineExtensions { public static IRqlEngine GetRqlEngine(this IRulesEngine rulesEngine) { @@ -11,19 +12,24 @@ public static IRqlEngine GetRqlEngine(this IRulesE public static IRqlEngine GetRqlEngine(this IRulesEngine rulesEngine, RqlOptions rqlOptions) { - if (!typeof(TContentType).IsEnum) + if (!IsSupportedType(typeof(TContentType))) { - throw new NotSupportedException($"Rule Query Language is not supported for non-enum types of {nameof(TContentType)}."); + throw new NotSupportedException($"Rule Query Language is only supported for enum types or strings on {nameof(TContentType)}."); } - if (!typeof(TConditionType).IsEnum) + if (!IsSupportedType(typeof(TConditionType))) { - throw new NotSupportedException($"Rule Query Language is not supported for non-enum types of {nameof(TConditionType)}."); + throw new NotSupportedException($"Rule Query Language is only supported for enum types or strings on {nameof(TConditionType)}."); } return RqlEngineBuilder.CreateRqlEngine(rulesEngine) .WithOptions(rqlOptions) .Build(); } + + private static bool IsSupportedType(Type type) + { + return type.IsEnum || type == typeof(string); + } } } \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Dto/RqlErrorDto.cs b/src/Rules.Framework.WebUI/Dto/RqlErrorDto.cs new file mode 100644 index 00000000..b745bb09 --- /dev/null +++ b/src/Rules.Framework.WebUI/Dto/RqlErrorDto.cs @@ -0,0 +1,15 @@ +namespace Rules.Framework.WebUI.Dto +{ + internal class RqlErrorDto + { + public int BeginPositionColumn { get; set; } + + public int BeginPositionLine { get; set; } + + public int EndPositionLine { get; set; } + + public int EndPositionColumn { get; set; } + + public string Message { get; set; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Dto/RqlInputDto.cs b/src/Rules.Framework.WebUI/Dto/RqlInputDto.cs new file mode 100644 index 00000000..52feddd2 --- /dev/null +++ b/src/Rules.Framework.WebUI/Dto/RqlInputDto.cs @@ -0,0 +1,7 @@ +namespace Rules.Framework.WebUI.Dto +{ + public class RqlInputDto + { + public string Rql { get; set; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Dto/RqlOutputDto.cs b/src/Rules.Framework.WebUI/Dto/RqlOutputDto.cs new file mode 100644 index 00000000..cf1a4020 --- /dev/null +++ b/src/Rules.Framework.WebUI/Dto/RqlOutputDto.cs @@ -0,0 +1,11 @@ +namespace Rules.Framework.WebUI.Dto +{ + using System.Collections.Generic; + + internal class RqlOutputDto + { + public string StandardOutput { get; set; } + + public IEnumerable StatementResults { get; set; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Dto/RqlRuntimeValueDto.cs b/src/Rules.Framework.WebUI/Dto/RqlRuntimeValueDto.cs new file mode 100644 index 00000000..4d7f3e4a --- /dev/null +++ b/src/Rules.Framework.WebUI/Dto/RqlRuntimeValueDto.cs @@ -0,0 +1,11 @@ +namespace Rules.Framework.WebUI.Dto +{ + public class RqlRuntimeValueDto + { + public string DisplayValue { get; set; } + + public object RuntimeValue { get; set; } + + public RqlTypeDto Type { get; set; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Dto/RqlStatementOutputDto.cs b/src/Rules.Framework.WebUI/Dto/RqlStatementOutputDto.cs new file mode 100644 index 00000000..a1168623 --- /dev/null +++ b/src/Rules.Framework.WebUI/Dto/RqlStatementOutputDto.cs @@ -0,0 +1,17 @@ +namespace Rules.Framework.WebUI.Dto +{ + using System.Collections.Generic; + + internal class RqlStatementOutputDto + { + public RqlErrorDto Error { get; set; } + + public bool IsSuccess { get; set; } + + public string Rql { get; set; } + + public IEnumerable Rules { get; set; } + + public object Value { get; set; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Dto/RqlTypeDto.cs b/src/Rules.Framework.WebUI/Dto/RqlTypeDto.cs new file mode 100644 index 00000000..fafba690 --- /dev/null +++ b/src/Rules.Framework.WebUI/Dto/RqlTypeDto.cs @@ -0,0 +1,7 @@ +namespace Rules.Framework.WebUI.Dto +{ + public class RqlTypeDto + { + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Extensions/RqlOutputDtoExtensions.cs b/src/Rules.Framework.WebUI/Extensions/RqlOutputDtoExtensions.cs new file mode 100644 index 00000000..b9112cfd --- /dev/null +++ b/src/Rules.Framework.WebUI/Extensions/RqlOutputDtoExtensions.cs @@ -0,0 +1,80 @@ +namespace Rules.Framework.WebUI.Extensions +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Rules.Framework.Generics; + using Rules.Framework.Rql; + using Rules.Framework.Rql.Runtime.Types; + using Rules.Framework.WebUI.Dto; + + internal static class RqlOutputDtoExtensions + { + public static RqlOutputDto ToRqlOutput( + this IEnumerable genericRqlResult, + IRuleStatusDtoAnalyzer ruleStatusDtoAnalyzer, + string standardOutput) + { + var rqlStatementOutputs = genericRqlResult.Select(grr => grr switch + { + NothingResult => new RqlStatementOutputDto { Rql = grr.Rql, Rules = null, Value = null }, + RulesSetResult grsrr => new RqlStatementOutputDto { Rql = grsrr.Rql, Rules = grsrr.Lines.Select(l => l.Rule.Value.ToRuleDto(ruleStatusDtoAnalyzer)), Value = null }, + ValueResult gvrr => new RqlStatementOutputDto { Rql = gvrr.Rql, Rules = null, Value = GetValue(gvrr) }, + _ => throw new NotSupportedException($"The RQL result of type '{grr.GetType().FullName}' is not supported."), + }); + + return new RqlOutputDto + { + StandardOutput = standardOutput, + StatementResults = rqlStatementOutputs, + }; + } + + public static RqlOutputDto ToRqlOutput( + this RqlException rqlException) + { + var rqlStatementOutputs = rqlException.Errors.Select(re => new RqlStatementOutputDto + { + IsSuccess = false, + Rql = re.Rql, + Rules = null, + Value = null, + Error = new RqlErrorDto + { + BeginPositionColumn = (int)re.BeginPosition.Column, + BeginPositionLine = (int)re.BeginPosition.Line, + EndPositionLine = (int)re.EndPosition.Line, + EndPositionColumn = (int)re.EndPosition.Column, + Message = re.Text, + }, + }); + + return new RqlOutputDto + { + StandardOutput = null!, + StatementResults = rqlStatementOutputs, + }; + } + + private static object GetValue(ValueResult vr) + { + return vr.Value switch + { + RqlAny rqlAny => new RqlRuntimeValueDto { DisplayValue = rqlAny.ToString(), RuntimeValue = rqlAny.RuntimeValue, Type = rqlAny.Type.ToRqlTypeDto() }, + RqlArray rqlArray => new RqlRuntimeValueDto { DisplayValue = rqlArray.ToString(), RuntimeValue = rqlArray.RuntimeValue, Type = rqlArray.Type.ToRqlTypeDto() }, + RqlBool rqlBool => new RqlRuntimeValueDto { DisplayValue = rqlBool.ToString(), RuntimeValue = rqlBool.RuntimeValue, Type = rqlBool.Type.ToRqlTypeDto() }, + RqlDate rqlDate => new RqlRuntimeValueDto { DisplayValue = rqlDate.ToString(), RuntimeValue = rqlDate.RuntimeValue, Type = rqlDate.Type.ToRqlTypeDto() }, + RqlDecimal rqlDecimal => new RqlRuntimeValueDto { DisplayValue = rqlDecimal.ToString(), RuntimeValue = rqlDecimal.RuntimeValue, Type = rqlDecimal.Type.ToRqlTypeDto() }, + RqlInteger rqlInteger => new RqlRuntimeValueDto { DisplayValue = rqlInteger.ToString(), RuntimeValue = rqlInteger.RuntimeValue, Type = rqlInteger.Type.ToRqlTypeDto() }, + RqlNothing rqlNothing => new RqlRuntimeValueDto { DisplayValue = rqlNothing.ToString(), RuntimeValue = rqlNothing.RuntimeValue, Type = rqlNothing.Type.ToRqlTypeDto() }, + RqlObject rqlObject => new RqlRuntimeValueDto { DisplayValue = rqlObject.ToString(), RuntimeValue = rqlObject.RuntimeValue, Type = rqlObject.Type.ToRqlTypeDto() }, + RqlReadOnlyObject rqlReadOnlyObject => new RqlRuntimeValueDto { DisplayValue = rqlReadOnlyObject.ToString(), RuntimeValue = rqlReadOnlyObject.RuntimeValue, Type = rqlReadOnlyObject.Type.ToRqlTypeDto() }, + RqlString rqlString => new RqlRuntimeValueDto { DisplayValue = rqlString.ToString(), RuntimeValue = rqlString.RuntimeValue, Type = rqlString.Type.ToRqlTypeDto() }, + _ => vr.Value, + }; + } + + private static RqlTypeDto ToRqlTypeDto(this RqlType rqlType) + => new RqlTypeDto { Name = rqlType.Name }; + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Extensions/RuleDtoExtensions.cs b/src/Rules.Framework.WebUI/Extensions/RuleDtoExtensions.cs index e50298fc..0e9f7fec 100644 --- a/src/Rules.Framework.WebUI/Extensions/RuleDtoExtensions.cs +++ b/src/Rules.Framework.WebUI/Extensions/RuleDtoExtensions.cs @@ -2,30 +2,31 @@ namespace Rules.Framework.WebUI.Extensions { using System.Collections.Generic; using System.Linq; - using Rules.Framework.Generics; + using Rules.Framework.Core; + using Rules.Framework.Core.ConditionNodes; using Rules.Framework.WebUI.Dto; internal static class RuleDtoExtensions { private const string dateFormat = "dd/MM/yyyy HH:mm:ss"; - public static ConditionNodeDto ToConditionNodeDto(this GenericConditionNode rootCondition) + public static ConditionNodeDto ToConditionNodeDto(this IConditionNode rootCondition) { if (rootCondition.LogicalOperator == Core.LogicalOperators.Eval || rootCondition.LogicalOperator == 0) { - var condition = rootCondition as GenericValueConditionNode; + var condition = (ValueConditionNode)rootCondition; return new ValueConditionNodeDto { - ConditionTypeName = condition.ConditionTypeName, + ConditionTypeName = condition.ConditionType, DataType = condition.DataType.ToString(), Operand = condition.Operand, Operator = condition.Operator.ToString(), }; } - var composedConditionNode = rootCondition as GenericComposedConditionNode; + var composedConditionNode = (ComposedConditionNode)rootCondition; var conditionNodeDataModels = new List(composedConditionNode.ChildConditionNodes.Count()); @@ -41,19 +42,19 @@ public static ConditionNodeDto ToConditionNodeDto(this GenericConditionNode root }; } - public static RuleDto ToRuleDto(this GenericRule rule, string ContentType, IRuleStatusDtoAnalyzer ruleStatusDtoAnalyzer) + public static RuleDto ToRuleDto(this Rule rule, IRuleStatusDtoAnalyzer ruleStatusDtoAnalyzer) { return new RuleDto { Conditions = rule.RootCondition?.ToConditionNodeDto(), - ContentType = ContentType, + ContentType = rule.ContentContainer.ContentType, Priority = rule.Priority, Name = rule.Name, - Value = rule.Content, + Value = rule.ContentContainer.GetContentAs(), DateEnd = !rule.DateEnd.HasValue ? null : rule.DateEnd.Value.ToString(dateFormat), DateBegin = rule.DateBegin.ToString(dateFormat), Status = !rule.Active ? RuleStatusDto.Deactivated.ToString() : ruleStatusDtoAnalyzer.Analyze(rule.DateBegin, rule.DateEnd).ToString(), }; } } -} +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Handlers/GetContentTypeHandler.cs b/src/Rules.Framework.WebUI/Handlers/GetContentTypeHandler.cs index b4464799..7b40f2c9 100644 --- a/src/Rules.Framework.WebUI/Handlers/GetContentTypeHandler.cs +++ b/src/Rules.Framework.WebUI/Handlers/GetContentTypeHandler.cs @@ -6,6 +6,7 @@ namespace Rules.Framework.WebUI.Handlers using System.Net; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; + using Rules.Framework.Core; using Rules.Framework.Generics; using Rules.Framework.WebUI.Dto; @@ -30,31 +31,29 @@ protected override async Task HandleRequestAsync(HttpRequest httpRequest, HttpRe { try { - var contents = this.genericRulesEngine.GetContentTypes(); + var contentTypes = this.genericRulesEngine.GetContentTypes(); - var contentTypes = new List(); + var contentTypeDtos = new List(); var index = 0; - foreach (var identifier in contents.Select(c => c.Identifier)) + foreach (var contentType in contentTypes) { - var genericContentType = new GenericContentType { Identifier = identifier }; - var genericRules = await this.genericRulesEngine - .SearchAsync(new SearchArgs(genericContentType, + .SearchAsync(new SearchArgs(contentType, DateTime.MinValue, DateTime.MaxValue)) .ConfigureAwait(false); - contentTypes.Add(new ContentTypeDto + contentTypeDtos.Add(new ContentTypeDto { Index = index, - Name = identifier, + Name = contentType, ActiveRulesCount = genericRules.Count(IsActive), RulesCount = genericRules.Count() }); index++; } - await this.WriteResponseAsync(httpResponse, contentTypes, (int)HttpStatusCode.OK).ConfigureAwait(false); + await this.WriteResponseAsync(httpResponse, contentTypeDtos, (int)HttpStatusCode.OK).ConfigureAwait(false); } catch (Exception ex) { @@ -62,9 +61,9 @@ protected override async Task HandleRequestAsync(HttpRequest httpRequest, HttpRe } } - private bool IsActive(GenericRule genericRule) + private bool IsActive(Rule genericRule) { return this.ruleStatusDtoAnalyzer.Analyze(genericRule.DateBegin, genericRule.DateEnd) == RuleStatusDto.Active; } } -} +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Handlers/GetRulesHandler.cs b/src/Rules.Framework.WebUI/Handlers/GetRulesHandler.cs index cf7c00d7..bef1da74 100644 --- a/src/Rules.Framework.WebUI/Handlers/GetRulesHandler.cs +++ b/src/Rules.Framework.WebUI/Handlers/GetRulesHandler.cs @@ -46,11 +46,11 @@ protected override async Task HandleRequestAsync(HttpRequest httpRequest, if (rulesFilter.ContentType.Equals("all")) { - var contents = this.genericRulesEngine.GetContentTypes(); + var contentTypes = this.genericRulesEngine.GetContentTypes(); - foreach (var identifier in contents.Select(c => c.Identifier)) + foreach (var contentType in contentTypes) { - var rulesForContentType = await this.GetRulesForContentyType(identifier, rulesFilter).ConfigureAwait(false); + var rulesForContentType = await this.GetRulesForContentyType(contentType, rulesFilter).ConfigureAwait(false); rules.AddRange(rulesForContentType); } } @@ -130,8 +130,8 @@ private RulesFilterDto GetRulesFilterFromRequest(HttpRequest httpRequest) private async Task> GetRulesForContentyType(string identifier, RulesFilterDto rulesFilter) { var genericRules = await this.genericRulesEngine.SearchAsync( - new SearchArgs( - new GenericContentType { Identifier = identifier }, + new SearchArgs( + identifier, rulesFilter.DateBegin.Value, rulesFilter.DateEnd.Value)) .ConfigureAwait(false); @@ -148,7 +148,7 @@ private async Task> GetRulesForContentyType(string identifi genericRules = genericRules.OrderBy(r => r.Priority); } - var genericRulesDto = this.ApplyFilters(rulesFilter, genericRules.Select(g => g.ToRuleDto(identifier, this.ruleStatusDtoAnalyzer))); + var genericRulesDto = this.ApplyFilters(rulesFilter, genericRules.Select(g => g.ToRuleDto(this.ruleStatusDtoAnalyzer))); return genericRulesDto; } diff --git a/src/Rules.Framework.WebUI/Handlers/PostRqlHandler.cs b/src/Rules.Framework.WebUI/Handlers/PostRqlHandler.cs new file mode 100644 index 00000000..3a7d5438 --- /dev/null +++ b/src/Rules.Framework.WebUI/Handlers/PostRqlHandler.cs @@ -0,0 +1,61 @@ +namespace Rules.Framework.WebUI.Handlers +{ + using System; + using System.IO; + using System.Net; + using System.Text.Json; + using System.Threading.Tasks; + using Microsoft.AspNetCore.Http; + using Rules.Framework.Generics; + using Rules.Framework.Rql; + using Rules.Framework.WebUI.Dto; + using Rules.Framework.WebUI.Extensions; + + internal class PostRqlHandler : WebUIRequestHandlerBase + { + private static readonly string[] resourcePaths = new[] { "/{0}/api/v1/rql" }; + private readonly IGenericRulesEngine genericRulesEngine; + private readonly IRuleStatusDtoAnalyzer ruleStatusDtoAnalyzer; + + public PostRqlHandler(IGenericRulesEngine genericRulesEngine, IRuleStatusDtoAnalyzer ruleStatusDtoAnalyzer, WebUIOptions webUIOptions) + : base(resourcePaths, webUIOptions) + { + this.genericRulesEngine = genericRulesEngine; + this.ruleStatusDtoAnalyzer = ruleStatusDtoAnalyzer; + } + + protected override HttpMethod HttpMethod => HttpMethod.POST; + + protected override async Task HandleRequestAsync( + HttpRequest httpRequest, + HttpResponse httpResponse, + RequestDelegate next) + { + var request = await JsonSerializer.DeserializeAsync(utf8Json: httpRequest.Body).ConfigureAwait(false); + try + { + var textWriter = new StringWriter(); + var rqlOptions = new RqlOptions + { + OutputWriter = textWriter, + }; + + using (var genericRqlEngine = this.genericRulesEngine.GetRqlEngine(rqlOptions)) + { + var genericRqlResult = await genericRqlEngine.ExecuteAsync(request.Rql).ConfigureAwait(false); + var response = genericRqlResult.ToRqlOutput(this.ruleStatusDtoAnalyzer, textWriter.GetStringBuilder().ToString()); + await this.WriteResponseAsync(httpResponse, response, (int)HttpStatusCode.OK).ConfigureAwait(false); + } + } + catch (RqlException rqlException) + { + var response = rqlException.ToRqlOutput(); + await this.WriteResponseAsync(httpResponse, response, (int)HttpStatusCode.BadRequest).ConfigureAwait(false); + } + catch (Exception ex) + { + await this.WriteExceptionResponseAsync(httpResponse, ex).ConfigureAwait(false); + } + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework.WebUI/Rules.Framework.WebUI.csproj b/src/Rules.Framework.WebUI/Rules.Framework.WebUI.csproj index 50e7336a..5de4cbfd 100644 --- a/src/Rules.Framework.WebUI/Rules.Framework.WebUI.csproj +++ b/src/Rules.Framework.WebUI/Rules.Framework.WebUI.csproj @@ -37,6 +37,7 @@ + diff --git a/src/Rules.Framework.WebUI/WebUIApplicationBuilderExtensions.cs b/src/Rules.Framework.WebUI/WebUIApplicationBuilderExtensions.cs index ab9d7105..f1aa9228 100644 --- a/src/Rules.Framework.WebUI/WebUIApplicationBuilderExtensions.cs +++ b/src/Rules.Framework.WebUI/WebUIApplicationBuilderExtensions.cs @@ -93,7 +93,8 @@ private static IApplicationBuilder UseRulesFrameworkWebUI(this IApplicationBuild new GetIndexPageHandler(webUIOptions), new GetConfigurationsHandler(genericRulesEngine, webUIOptions), new GetContentTypeHandler(genericRulesEngine, ruleStatusDtoAnalyzer, webUIOptions), - new GetRulesHandler(genericRulesEngine, ruleStatusDtoAnalyzer, webUIOptions) + new GetRulesHandler(genericRulesEngine, ruleStatusDtoAnalyzer, webUIOptions), + new PostRqlHandler(genericRulesEngine, ruleStatusDtoAnalyzer, webUIOptions), }, webUIOptions); diff --git a/src/Rules.Framework.WebUI/index.html b/src/Rules.Framework.WebUI/index.html index 13fe9a1d..781c19fe 100644 --- a/src/Rules.Framework.WebUI/index.html +++ b/src/Rules.Framework.WebUI/index.html @@ -54,6 +54,54 @@ --tab-left: 4em; padding-left: var(--tab-left, 4em); } + + .rql-label { + display: block; + color: #fff; + font-size: 1.5rem; + padding: 0px 15px 0px 0px; + } + + .rql-code-editor { + display: inline-flex; + gap: 10px; + font-family: monospace; + line-height: 21px; + background: RGBA(33,37,41,var(--bs-bg-opacity,1)) !important; + border-radius: 3px; + padding: 0px 0px 0px 10px; + } + + .rql-line-numbers { + width: 20px; + text-align: right; + padding: 10px 0px 0px 0px; + } + + .rql-line-numbers span { + counter-increment: linenumber; + } + + .rql-line-numbers span::before { + content: counter(linenumber); + display: block; + color: #fff; + } + + .rql-text-area { + line-height: 21px; + overflow-y: hidden; + padding: 10px 0px 0px 0px; + border: 1px solid #ced4da; + border-radius: 3px; + background: #fff; + color: #212529; + min-width: 500px; + outline: none; + resize: none; + width: 100%; + tab-size: 4; + } @@ -82,6 +130,11 @@ Rules + @@ -118,8 +171,9 @@
-
@@ -182,7 +236,7 @@
Content Types
- +
@@ -252,6 +306,86 @@
Rules
+ +
@@ -265,6 +399,7 @@
Rules
diff --git a/src/Rules.Framework/Extensions/GenericRuleExtensions.cs b/src/Rules.Framework/Extensions/GenericRuleExtensions.cs deleted file mode 100644 index 7ed33f25..00000000 --- a/src/Rules.Framework/Extensions/GenericRuleExtensions.cs +++ /dev/null @@ -1,57 +0,0 @@ -namespace Rules.Framework.Extensions -{ - using System.Collections.Generic; - using System.Linq; - using Rules.Framework.Core; - using Rules.Framework.Core.ConditionNodes; - using Rules.Framework.Generics; - - internal static class GenericRuleExtensions - { - public static GenericConditionNode ToGenericConditionNode(this IConditionNode rootCondition) - { - if (rootCondition.LogicalOperator == LogicalOperators.Eval) - { - var condition = rootCondition as ValueConditionNode; - - return new GenericValueConditionNode - { - ConditionTypeName = condition.ConditionType.ToString(), - DataType = condition.DataType, - LogicalOperator = condition.LogicalOperator, - Operand = condition.Operand, - Operator = condition.Operator, - }; - } - - var composedConditionNode = rootCondition as ComposedConditionNode; - - var conditionNodeDataModels = new List(composedConditionNode.ChildConditionNodes.Count()); - - foreach (var child in composedConditionNode.ChildConditionNodes) - { - conditionNodeDataModels.Add(child.ToGenericConditionNode()); - } - - return new GenericComposedConditionNode - { - ChildConditionNodes = conditionNodeDataModels, - LogicalOperator = composedConditionNode.LogicalOperator, - }; - } - - public static GenericRule ToGenericRule(this Rule rule) - { - return new GenericRule - { - RootCondition = rule.RootCondition?.ToGenericConditionNode(), - Content = rule.ContentContainer.GetContentAs(), - DateBegin = rule.DateBegin, - DateEnd = rule.DateEnd, - Name = rule.Name, - Priority = rule.Priority, - Active = rule.Active, - }; - } - } -} diff --git a/src/Rules.Framework/Extensions/RuleExtensions.cs b/src/Rules.Framework/Extensions/RuleExtensions.cs new file mode 100644 index 00000000..5008e07b --- /dev/null +++ b/src/Rules.Framework/Extensions/RuleExtensions.cs @@ -0,0 +1,106 @@ +namespace Rules.Framework.Extensions +{ + using System; + using System.Collections.Generic; + using System.Linq; + using Rules.Framework.Core; + using Rules.Framework.Core.ConditionNodes; + using Rules.Framework.Generics; + + internal static class RuleExtensions + { + public static IConditionNode ToConcreteConditionNode(this IConditionNode rootCondition) + { + if (rootCondition.LogicalOperator == LogicalOperators.Eval) + { + var condition = (ValueConditionNode)rootCondition; + + return new ValueConditionNode( + condition.DataType, + condition.ConditionType!.ToConcreteConditionType(), + condition.Operator, + condition.Operand, + condition.Properties); + } + + var composedConditionNode = (ComposedConditionNode)rootCondition; + + var childConditionNodes = new List>(composedConditionNode.ChildConditionNodes.Count()); + + foreach (var child in composedConditionNode.ChildConditionNodes) + { + childConditionNodes.Add(child.ToConcreteConditionNode()); + } + + return new ComposedConditionNode(composedConditionNode.LogicalOperator, childConditionNodes); + } + + public static TContentType ToConcreteConditionType(this string contentType) + { + return (TContentType)Enum.Parse(typeof(TContentType), contentType); + } + + public static TConditionType ToConcreteContentType(this string conditionType) + { + return (TConditionType)Enum.Parse(typeof(TConditionType), conditionType); + } + + public static Rule ToConcreteRule(this Rule rule) + { + return new Rule + { + RootCondition = rule.RootCondition?.ToConcreteConditionNode()!, + ContentContainer = new ContentContainer( + rule.ContentContainer.ContentType.ToConcreteContentType(), + (t) => rule.ContentContainer.GetContentAs()), + DateBegin = rule.DateBegin, + DateEnd = rule.DateEnd, + Name = rule.Name, + Priority = rule.Priority, + Active = rule.Active, + }; + } + + public static IConditionNode ToGenericConditionNode(this IConditionNode rootCondition) + { + if (rootCondition.LogicalOperator == LogicalOperators.Eval) + { + var condition = (ValueConditionNode)rootCondition; + + return new ValueConditionNode( + condition.DataType, + condition.ConditionType!.ToString(), + condition.Operator, + condition.Operand, + condition.Properties); + } + + var composedConditionNode = (ComposedConditionNode)rootCondition; + + var childConditionNodes = new List>(composedConditionNode.ChildConditionNodes.Count()); + + foreach (var child in composedConditionNode.ChildConditionNodes) + { + childConditionNodes.Add(child.ToGenericConditionNode()); + } + + return new ComposedConditionNode(composedConditionNode.LogicalOperator, childConditionNodes); + } + + public static Rule ToGenericRule(this Rule rule) + { + return new Rule + { + RootCondition = rule.RootCondition?.ToGenericConditionNode()!, + ContentContainer = new ContentContainer( + rule.ContentContainer.ContentType!.ToString(), + (t) => rule.ContentContainer.GetContentAs()), + DateBegin = rule.DateBegin, + DateEnd = rule.DateEnd, + Name = rule.Name, + Priority = rule.Priority, + Active = rule.Active, + }; + } + } +} \ No newline at end of file diff --git a/src/Rules.Framework/Extensions/RulesEngineExtensions.cs b/src/Rules.Framework/Extensions/RulesEngineExtensions.cs index 60ce6941..425c9def 100644 --- a/src/Rules.Framework/Extensions/RulesEngineExtensions.cs +++ b/src/Rules.Framework/Extensions/RulesEngineExtensions.cs @@ -1,5 +1,6 @@ namespace Rules.Framework.Extension { + using System; using Rules.Framework.Generics; /// @@ -16,6 +17,11 @@ public static class RulesEngineExtensions /// A new instance of generic engine public static IGenericRulesEngine CreateGenericEngine(this RulesEngine rulesEngine) { + if (!typeof(TContentType).IsEnum) + { + throw new NotSupportedException($"Generic rules engine is only supported for enum types of {nameof(TContentType)}."); + } + return new GenericRulesEngine(rulesEngine); } } diff --git a/src/Rules.Framework/Extensions/GenericSearchArgsExtensions.cs b/src/Rules.Framework/Extensions/SearchArgsExtensions.cs similarity index 84% rename from src/Rules.Framework/Extensions/GenericSearchArgsExtensions.cs rename to src/Rules.Framework/Extensions/SearchArgsExtensions.cs index dcb0c839..368beb86 100644 --- a/src/Rules.Framework/Extensions/GenericSearchArgsExtensions.cs +++ b/src/Rules.Framework/Extensions/SearchArgsExtensions.cs @@ -4,17 +4,17 @@ namespace Rules.Framework.Extensions using System.Linq; using Rules.Framework.Generics; - internal static class GenericSearchArgsExtensions + internal static class SearchArgsExtensions { - public static SearchArgs ToSearchArgs( - this SearchArgs genericSearchArgs) + public static SearchArgs ToGenericSearchArgs( + this SearchArgs genericSearchArgs) { if (!typeof(TContentType).IsEnum) { throw new ArgumentException("Only TContentType of type enum are currently supported."); } - var contentType = (TContentType)Enum.Parse(typeof(TContentType), genericSearchArgs.ContentType.Identifier); + var contentType = (TContentType)Enum.Parse(typeof(TContentType), genericSearchArgs.ContentType); if (genericSearchArgs.Active.HasValue) { @@ -22,7 +22,7 @@ public static SearchArgs ToSearchArgs new Condition ( - (TConditionType)Enum.Parse(typeof(TConditionType), condition.Type.Identifier), + (TConditionType)Enum.Parse(typeof(TConditionType), condition.Type), condition.Value )).ToList(), ExcludeRulesWithoutSearchConditions = genericSearchArgs.ExcludeRulesWithoutSearchConditions @@ -33,7 +33,7 @@ public static SearchArgs ToSearchArgs new Condition ( - (TConditionType)Enum.Parse(typeof(TConditionType), condition.Type.Identifier), + (TConditionType)Enum.Parse(typeof(TConditionType), condition.Type), condition.Value )).ToList(), ExcludeRulesWithoutSearchConditions = genericSearchArgs.ExcludeRulesWithoutSearchConditions diff --git a/src/Rules.Framework/Generics/GenericComposedConditionNode.cs b/src/Rules.Framework/Generics/GenericComposedConditionNode.cs deleted file mode 100644 index 38901d2c..00000000 --- a/src/Rules.Framework/Generics/GenericComposedConditionNode.cs +++ /dev/null @@ -1,16 +0,0 @@ -namespace Rules.Framework.Generics -{ - using System.Collections.Generic; - - /// - /// Defines generic condition node - /// - /// - public sealed class GenericComposedConditionNode : GenericConditionNode - { - /// - /// Gets the child condition nodes. - /// - public IEnumerable ChildConditionNodes { get; internal set; } - } -} \ No newline at end of file diff --git a/src/Rules.Framework/Generics/GenericConditionNode.cs b/src/Rules.Framework/Generics/GenericConditionNode.cs deleted file mode 100644 index 2fab53c8..00000000 --- a/src/Rules.Framework/Generics/GenericConditionNode.cs +++ /dev/null @@ -1,15 +0,0 @@ -namespace Rules.Framework.Generics -{ - using Rules.Framework.Core; - - /// - /// Defines generic condition node - /// - public class GenericConditionNode - { - /// - /// Gets the logical operator to apply to condition node. - /// - public LogicalOperators LogicalOperator { get; internal set; } - } -} \ No newline at end of file diff --git a/src/Rules.Framework/Generics/GenericConditionType.cs b/src/Rules.Framework/Generics/GenericConditionType.cs deleted file mode 100644 index 1a0f9d4a..00000000 --- a/src/Rules.Framework/Generics/GenericConditionType.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Rules.Framework.Generics -{ - using System; - - /// - /// Defines generic condition type - /// - public struct GenericConditionType : IEquatable - { - /// - /// Gets or sets the identifier. - /// - /// - /// The identifier. - /// - public string Identifier { get; set; } - - /// - /// Indicates whether the current object is equal to another object of the same type. - /// - /// An object to compare with this object. - /// - /// true if the current object is equal to the other - /// parameter; otherwise, false. - /// - public bool Equals(GenericConditionType other) - { - return other.Identifier == this.Identifier; - } - } -} \ No newline at end of file diff --git a/src/Rules.Framework/Generics/GenericContentType.cs b/src/Rules.Framework/Generics/GenericContentType.cs deleted file mode 100644 index f6c6ae46..00000000 --- a/src/Rules.Framework/Generics/GenericContentType.cs +++ /dev/null @@ -1,31 +0,0 @@ -namespace Rules.Framework.Generics -{ - using System; - - /// - /// Defines generic content type - /// - public struct GenericContentType : IEquatable - { - /// - /// Gets or sets the identifier. - /// - /// - /// The identifier. - /// - public string Identifier { get; set; } - - /// - /// Indicates whether the current object is equal to another object of the same type. - /// - /// An object to compare with this object. - /// - /// true if the current object is equal to the other - /// parameter; otherwise, false. - /// - public bool Equals(GenericContentType other) - { - return other.Identifier == this.Identifier; - } - } -} \ No newline at end of file diff --git a/src/Rules.Framework/Generics/GenericRule.cs b/src/Rules.Framework/Generics/GenericRule.cs deleted file mode 100644 index a6037ed7..00000000 --- a/src/Rules.Framework/Generics/GenericRule.cs +++ /dev/null @@ -1,45 +0,0 @@ -namespace Rules.Framework.Generics -{ - using System; - - /// - /// Defines a generic rule - /// - public sealed class GenericRule - { - /// - /// Gets and sets the if the rules ia active. - /// - public bool Active { get; internal set; } - - /// - /// Gets the content which contains the rule content. - /// - public object Content { get; internal set; } - - /// - /// Gets the date from which the rule begins being applicable. - /// - public DateTime DateBegin { get; internal set; } - - /// - /// Gets and sets the date from which the rule ceases to be applicable. - /// - public DateTime? DateEnd { get; internal set; } - - /// - /// Gets the rule name. - /// - public string Name { get; internal set; } - - /// - /// Gets the rule priority compared to other rules (preferrably it is unique). - /// - public int Priority { get; internal set; } - - /// - /// Gets the rule root condition. This property is null when rule has no conditions. - /// - public GenericConditionNode RootCondition { get; internal set; } - } -} \ No newline at end of file diff --git a/src/Rules.Framework/Generics/GenericRulesEngine.cs b/src/Rules.Framework/Generics/GenericRulesEngine.cs index 956eaa41..e911fa6b 100644 --- a/src/Rules.Framework/Generics/GenericRulesEngine.cs +++ b/src/Rules.Framework/Generics/GenericRulesEngine.cs @@ -4,6 +4,7 @@ namespace Rules.Framework.Generics using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; + using Rules.Framework.Core; using Rules.Framework.Extensions; internal sealed class GenericRulesEngine : IGenericRulesEngine @@ -15,34 +16,68 @@ public GenericRulesEngine(IRulesEngine rulesEngine this.rulesEngine = rulesEngine; } - public IEnumerable GetContentTypes() + public async Task AddRuleAsync(Rule rule, RuleAddPriorityOption ruleAddPriorityOption) { - if (!typeof(TContentType).IsEnum) - { - throw new ArgumentException("Method only works if TContentType is a enum"); - } + var concreteRule = rule.ToConcreteRule(); - return Enum.GetValues(typeof(TContentType)) - .Cast() - .Select(t => new GenericContentType - { - Identifier = Enum.Parse(typeof(TContentType), t.ToString()).ToString() - }); + return await this.rulesEngine.AddRuleAsync(concreteRule, ruleAddPriorityOption).ConfigureAwait(false); } + public IEnumerable GetContentTypes() + { + return Enum.GetValues(typeof(TContentType)) + .Cast() + .Select(t => t.ToString()); + } public PriorityCriterias GetPriorityCriteria() { return this.rulesEngine.GetPriorityCriteria(); } - public async Task> SearchAsync(SearchArgs genericSearchArgs) + public async Task> GetUniqueConditionTypesAsync(string contentType, DateTime dateBegin, DateTime dateEnd) + { + var concreteContentType = contentType.ToConcreteContentType(); + + var conditionTypes = await this.rulesEngine.GetUniqueConditionTypesAsync(concreteContentType, dateBegin, dateEnd).ConfigureAwait(false); + + return conditionTypes.Select(ct => ct!.ToString()).ToArray(); + } + + public async Task>> MatchManyAsync(string contentType, DateTime matchDateTime, IEnumerable> conditions) { - var searchArgs = genericSearchArgs.ToSearchArgs(); + var concreteContentType = contentType.ToConcreteContentType(); + var concreteConditions = conditions.Select(c => new Condition(c.Type.ToConcreteConditionType(), c.Value)).ToArray(); + + var concreteRules = await this.rulesEngine.MatchManyAsync(concreteContentType, matchDateTime, concreteConditions).ConfigureAwait(false); + + return concreteRules.Select(r => r.ToGenericRule()).ToArray(); + } - var result = await this.rulesEngine.SearchAsync(searchArgs).ConfigureAwait(false); + public async Task> MatchOneAsync(string contentType, DateTime matchDateTime, IEnumerable> conditions) + { + var concreteContentType = contentType.ToConcreteContentType(); + var concreteConditions = conditions.Select(c => new Condition(c.Type.ToConcreteConditionType(), c.Value)).ToArray(); + + var concreteRule = await this.rulesEngine.MatchOneAsync(concreteContentType, matchDateTime, concreteConditions).ConfigureAwait(false); + + return concreteRule.ToGenericRule(); + } + + public async Task>> SearchAsync(SearchArgs searchArgs) + { + var innerSearchArgs = searchArgs.ToGenericSearchArgs(); + + var result = await this.rulesEngine.SearchAsync(innerSearchArgs).ConfigureAwait(false); + + return result.Select(rule => rule.ToGenericRule()).ToArray(); + } + + public async Task UpdateRuleAsync(Rule rule) + { + var concreteRule = rule.ToConcreteRule(); - return result.Select(rule => rule.ToGenericRule()).ToList(); + return await this.rulesEngine.UpdateRuleAsync(concreteRule).ConfigureAwait(false); } } } \ No newline at end of file diff --git a/src/Rules.Framework/Generics/GenericValueConditionNode.cs b/src/Rules.Framework/Generics/GenericValueConditionNode.cs deleted file mode 100644 index 2d4b7bc3..00000000 --- a/src/Rules.Framework/Generics/GenericValueConditionNode.cs +++ /dev/null @@ -1,30 +0,0 @@ -namespace Rules.Framework.Generics -{ - using Rules.Framework.Core; - - /// - /// Defines generic value condition node - /// - public sealed class GenericValueConditionNode : GenericConditionNode - { - /// - /// Gets the condition node type name. - /// - public string ConditionTypeName { get; internal set; } - - /// - /// Gets the condition node data type. - /// - public DataTypes DataType { get; internal set; } - - /// - /// Gets the condition's operand. - /// - public object Operand { get; internal set; } - - /// - /// Gets the condition node operator. - /// - public Operators Operator { get; internal set; } - } -} \ No newline at end of file diff --git a/src/Rules.Framework/Generics/IGenericRulesEngine.cs b/src/Rules.Framework/Generics/IGenericRulesEngine.cs index 9c0e322d..99cc748b 100644 --- a/src/Rules.Framework/Generics/IGenericRulesEngine.cs +++ b/src/Rules.Framework/Generics/IGenericRulesEngine.cs @@ -6,7 +6,7 @@ namespace Rules.Framework.Generics /// /// Exposes generic rules engine logic to provide rule matches to requests. /// - public interface IGenericRulesEngine + public interface IGenericRulesEngine : IRulesEngine { /// /// Gets the content types. @@ -15,19 +15,6 @@ public interface IGenericRulesEngine /// /// Method only works if TContentType is a enum /// - IEnumerable GetContentTypes(); - - /// - /// Gets the priority criterias. - /// - /// Rules engine priority criterias - PriorityCriterias GetPriorityCriteria(); - - /// - /// Searches the asynchronous. - /// - /// The search arguments. - /// List of generic rules - Task> SearchAsync(SearchArgs genericSearchArgs); + IEnumerable GetContentTypes(); } } \ No newline at end of file diff --git a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.InputConditionSegment.cs b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.InputConditionSegment.cs index 91d71b78..633de90e 100644 --- a/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.InputConditionSegment.cs +++ b/tests/Rules.Framework.Rql.Tests/Pipeline/Interpret/InterpreterTests.InputConditionSegment.cs @@ -4,6 +4,7 @@ namespace Rules.Framework.Rql.Tests.Pipeline.Interpret using FluentAssertions; using Moq; using Rules.Framework.Rql; + using Rules.Framework.Rql.Ast.Expressions; using Rules.Framework.Rql.Ast.Segments; using Rules.Framework.Rql.Pipeline.Interpret; using Rules.Framework.Rql.Runtime; @@ -26,7 +27,7 @@ public async Task VisitInputConditionSegment_GivenInvalidConditionType_ThrowsInt var runtime = Mock.Of>(); var reverseRqlBuilder = Mock.Of(); Mock.Get(reverseRqlBuilder) - .Setup(x => x.BuildRql(It.IsIn(inputConditionSegment))) + .Setup(x => x.BuildRql(It.IsAny())) .Returns(expectedRql); var interpreter = new Interpreter(runtime, reverseRqlBuilder); diff --git a/tests/Rules.Framework.Rql.Tests/RulesEngineExtensionsTests.cs b/tests/Rules.Framework.Rql.Tests/RulesEngineExtensionsTests.cs index 2925d813..ef708d06 100644 --- a/tests/Rules.Framework.Rql.Tests/RulesEngineExtensionsTests.cs +++ b/tests/Rules.Framework.Rql.Tests/RulesEngineExtensionsTests.cs @@ -8,7 +8,7 @@ namespace Rules.Framework.Rql.Tests public class RulesEngineExtensionsTests { [Fact] - public void GetRqlEngine_GivenRulesEngine_BuildsRqlEngineWithDefaultRqlOptions() + public void GetRqlEngine_GivenRulesEngineWithEnumType_BuildsRqlEngineWithDefaultRqlOptions() { // Arrange var rulesEngine = Mock.Of>(); @@ -24,26 +24,39 @@ public void GetRqlEngine_GivenRulesEngine_BuildsRqlEngineWithDefaultRqlOptions() public void GetRqlEngine_GivenRulesEngineWithNonEnumConditionType_ThrowsNotSupportedException() { // Arrange - var rulesEngine = Mock.Of>(); + var rulesEngine = Mock.Of>(); // Act var notSupportedException = Assert.Throws(() => rulesEngine.GetRqlEngine()); // Assert - notSupportedException.Message.Should().Be("Rule Query Language is not supported for non-enum types of TConditionType."); + notSupportedException.Message.Should().Be("Rule Query Language is only supported for enum types or strings on TConditionType."); } [Fact] public void GetRqlEngine_GivenRulesEngineWithNonEnumContentType_ThrowsNotSupportedException() { // Arrange - var rulesEngine = Mock.Of>(); + var rulesEngine = Mock.Of>(); // Act var notSupportedException = Assert.Throws(() => rulesEngine.GetRqlEngine()); // Assert - notSupportedException.Message.Should().Be("Rule Query Language is not supported for non-enum types of TContentType."); + notSupportedException.Message.Should().Be("Rule Query Language is only supported for enum types or strings on TContentType."); + } + + [Fact] + public void GetRqlEngine_GivenRulesEngineWithStringTypes_BuildsRqlEngineWithDefaultRqlOptions() + { + // Arrange + var rulesEngine = Mock.Of>(); + + // Act + var rqlEngine = rulesEngine.GetRqlEngine(); + + // Assert + rqlEngine.Should().NotBeNull(); } } } \ No newline at end of file diff --git a/tests/Rules.Framework.Tests/Extensions/GenericRuleExtensionsTests.cs b/tests/Rules.Framework.Tests/Extensions/RuleExtensionsTests.cs similarity index 59% rename from tests/Rules.Framework.Tests/Extensions/GenericRuleExtensionsTests.cs rename to tests/Rules.Framework.Tests/Extensions/RuleExtensionsTests.cs index 90f66e09..cc308936 100644 --- a/tests/Rules.Framework.Tests/Extensions/GenericRuleExtensionsTests.cs +++ b/tests/Rules.Framework.Tests/Extensions/RuleExtensionsTests.cs @@ -7,55 +7,25 @@ namespace Rules.Framework.Tests.Extensions using Rules.Framework.Core; using Rules.Framework.Core.ConditionNodes; using Rules.Framework.Extensions; - using Rules.Framework.Generics; using Rules.Framework.Tests.Stubs; using Xunit; - public class GenericRuleExtensionsTests + public class RuleExtensionsTests { [Fact] public void GenericRuleExtensions_ToGenericRule_WithComposedCondition_Success() { var expectedRuleContent = "Type1"; - var expectedRootCondition = new GenericComposedConditionNode + var expectedRootCondition = new ComposedConditionNode(LogicalOperators.And, new List> { - ChildConditionNodes = new List + new ValueConditionNode(DataTypes.Boolean, ConditionType.IsVip.ToString(), Operators.Equal, true), + new ComposedConditionNode(LogicalOperators.Or, new List> { - new GenericValueConditionNode - { - ConditionTypeName = ConditionType.IsVip.ToString(), - DataType = DataTypes.Boolean, - LogicalOperator = LogicalOperators.Eval, - Operand = true, - Operator = Operators.Equal - }, - new GenericComposedConditionNode - { - ChildConditionNodes = new List - { - new GenericValueConditionNode - { - ConditionTypeName = ConditionType.IsoCurrency.ToString(), - DataType = DataTypes.String, - LogicalOperator = LogicalOperators.Eval, - Operand = "EUR", - Operator = Operators.Equal - }, - new GenericValueConditionNode - { - ConditionTypeName = ConditionType.IsoCurrency.ToString(), - DataType = DataTypes.String, - LogicalOperator = LogicalOperators.Eval, - Operand = "USD", - Operator = Operators.Equal - } - }, - LogicalOperator = LogicalOperators.Or - } - }, - LogicalOperator = LogicalOperators.And - }; + new ValueConditionNode(DataTypes.String, ConditionType.IsoCurrency.ToString(), Operators.Equal, "EUR"), + new ValueConditionNode(DataTypes.String, ConditionType.IsoCurrency.ToString(), Operators.Equal, "USD") + }) + }); var rootComposedCondition = new RootConditionNodeBuilder() .And(a => a @@ -82,12 +52,12 @@ public void GenericRuleExtensions_ToGenericRule_WithComposedCondition_Success() genericRule.Should().BeEquivalentTo(rule, config => config .Excluding(r => r.ContentContainer) .Excluding(r => r.RootCondition)); - genericRule.Content.Should().BeOfType(); - genericRule.Content.Should().Be(expectedRuleContent); - genericRule.RootCondition.Should().BeOfType(); + var content = genericRule.ContentContainer.GetContentAs(); + content.Should().Be(expectedRuleContent); + genericRule.RootCondition.Should().BeOfType>(); - var genericComposedRootCondition = genericRule.RootCondition as GenericComposedConditionNode; - genericComposedRootCondition.Should().BeEquivalentTo(expectedRootCondition, config => config.IncludingAllRuntimeProperties()); + var genericComposedRootCondition = genericRule.RootCondition as ComposedConditionNode; + genericComposedRootCondition.Should().BeEquivalentTo(expectedRootCondition, options => options.ComparingByMembers>()); } [Fact] @@ -108,8 +78,8 @@ public void GenericRuleExtensions_ToGenericRule_WithoutConditions_Success() // Assert genericRule.Should().BeEquivalentTo(rule, config => config.Excluding(r => r.ContentContainer)); - genericRule.Content.Should().BeOfType(); - genericRule.Content.Should().Be(expectedRuleContent); + var content = genericRule.ContentContainer.GetContentAs(); + content.Should().Be(expectedRuleContent); genericRule.RootCondition.Should().BeNull(); } @@ -135,12 +105,12 @@ public void GenericRuleExtensions_ToGenericRule_WithValueCondition_Success() genericRule.Should().BeEquivalentTo(rule, config => config .Excluding(r => r.ContentContainer) .Excluding(r => r.RootCondition)); - genericRule.Content.Should().BeOfType(); - genericRule.Content.Should().Be(expectedRuleContent); - genericRule.RootCondition.Should().BeOfType(); + var content = genericRule.ContentContainer.GetContentAs(); + content.Should().Be(expectedRuleContent); + genericRule.RootCondition.Should().BeOfType>(); - var genericValueRootCondition = genericRule.RootCondition as GenericValueConditionNode; - genericValueRootCondition.ConditionTypeName.Should().Be(expectedRootCondition.ConditionType.ToString()); + var genericValueRootCondition = genericRule.RootCondition as ValueConditionNode; + genericValueRootCondition.ConditionType.Should().Be(expectedRootCondition.ConditionType.ToString()); genericValueRootCondition.DataType.Should().Be(expectedRootCondition.DataType); genericValueRootCondition.LogicalOperator.Should().Be(expectedRootCondition.LogicalOperator); genericValueRootCondition.Operand.Should().Be(expectedRootCondition.Operand); diff --git a/tests/Rules.Framework.Tests/Extensions/GenericSearchArgsExtensionsTests.cs b/tests/Rules.Framework.Tests/Extensions/SearchArgsExtensionsTests.cs similarity index 60% rename from tests/Rules.Framework.Tests/Extensions/GenericSearchArgsExtensionsTests.cs rename to tests/Rules.Framework.Tests/Extensions/SearchArgsExtensionsTests.cs index a23f5549..5bdf2cd8 100644 --- a/tests/Rules.Framework.Tests/Extensions/GenericSearchArgsExtensionsTests.cs +++ b/tests/Rules.Framework.Tests/Extensions/SearchArgsExtensionsTests.cs @@ -4,11 +4,10 @@ namespace Rules.Framework.Tests.Extensions using System.Collections.Generic; using FluentAssertions; using Rules.Framework.Extensions; - using Rules.Framework.Generics; using Rules.Framework.Tests.Stubs; using Xunit; - public class GenericSearchArgsExtensionsTests + public class SearchArgsExtensionsTests { [Fact] public void GenericSearchArgsExtensions_ToSearchArgs_WithConditions_Success() @@ -24,28 +23,26 @@ public void GenericSearchArgsExtensions_ToSearchArgs_WithConditions_Success() { Conditions = new List> { - new Condition(ConditionType.PluviosityRate, pluviosityRate), - new Condition(ConditionType.IsoCountryCode, countryCode) + new(ConditionType.PluviosityRate, pluviosityRate), + new(ConditionType.IsoCountryCode, countryCode) }, ExcludeRulesWithoutSearchConditions = true }; var contentTypeCode = "Type1"; - var genericSearchArgs = new SearchArgs( - new GenericContentType { Identifier = contentTypeCode }, dateBegin, dateEnd - ) + var genericSearchArgs = new SearchArgs(contentTypeCode, dateBegin, dateEnd) { - Conditions = new List> + Conditions = new List> { - new Condition(new GenericConditionType { Identifier = "PluviosityRate" }, pluviosityRate), - new Condition(new GenericConditionType { Identifier = "IsoCountryCode" }, countryCode) + new("PluviosityRate", pluviosityRate), + new("IsoCountryCode", countryCode) }, ExcludeRulesWithoutSearchConditions = true }; // Act - var convertedSearchArgs = genericSearchArgs.ToSearchArgs(); + var convertedSearchArgs = genericSearchArgs.ToGenericSearchArgs(); // Assert convertedSearchArgs.Should().BeEquivalentTo(expectedSearchArgs); @@ -59,12 +56,10 @@ public void GenericSearchArgsExtensions_ToSearchArgs_WithInvalidType_ThrowsExcep var dateBegin = new DateTime(2018, 01, 01); var dateEnd = new DateTime(2020, 12, 31); - var genericSearchArgs = new SearchArgs( - new GenericContentType { Identifier = contentTypeCode }, dateBegin, dateEnd - ); + var genericSearchArgs = new SearchArgs(contentTypeCode, dateBegin, dateEnd); // Act and Assert - Assert.Throws(() => genericSearchArgs.ToSearchArgs()); + Assert.Throws(() => genericSearchArgs.ToGenericSearchArgs()); } [Fact] @@ -78,11 +73,10 @@ public void GenericSearchArgsExtensions_ToSearchArgs_WithoutConditions_Success() var expectedSearchArgs = new SearchArgs(contentType, dateBegin, dateEnd, active: true); - var genericSearchArgs = new SearchArgs( - new GenericContentType { Identifier = contentTypeCode }, dateBegin, dateEnd, active: true); + var genericSearchArgs = new SearchArgs(contentTypeCode, dateBegin, dateEnd, active: true); // Act - var convertedSearchArgs = genericSearchArgs.ToSearchArgs(); + var convertedSearchArgs = genericSearchArgs.ToGenericSearchArgs(); // Assert convertedSearchArgs.Should().BeEquivalentTo(expectedSearchArgs); diff --git a/tests/Rules.Framework.Tests/Generics/GenericRulesEngineTests.cs b/tests/Rules.Framework.Tests/Generics/GenericRulesEngineTests.cs index df107552..db8de80c 100644 --- a/tests/Rules.Framework.Tests/Generics/GenericRulesEngineTests.cs +++ b/tests/Rules.Framework.Tests/Generics/GenericRulesEngineTests.cs @@ -24,11 +24,7 @@ public GenericRulesEngineTests() public void GenericRulesEngine_GetContentTypes_Success() { // Arrange - var expectedGenericContentTypes = new List - { - new GenericContentType { Identifier = "Type1" }, - new GenericContentType { Identifier = "Type2" }, - }; + var expectedGenericContentTypes = new List { "Type1", "Type2" }; var genericRulesEngine = new GenericRulesEngine(this.mockRulesEngine.Object); @@ -88,37 +84,29 @@ public void GenericRulesEngine_GetPriorityCriterias_CallsRulesEngineMethod() public async Task GenericRulesEngine_SearchAsync_Success() { // Arrange - var expectedGenericRules = new List + var expectedGenericRules = new List> { - new GenericRule + new Rule { - Content = new ContentContainer(ContentType.Type1, (_) => new object()).GetContentAs(), + ContentContainer = new ContentContainer("Type1", (_) => new object()), DateBegin = new DateTime(2018, 01, 01), DateEnd = new DateTime(2019, 01, 01), Name = "Test rule", Priority = 3, Active = true, - RootCondition = new GenericValueConditionNode - { - ConditionTypeName = ConditionType.IsoCountryCode.ToString(), - DataType = DataTypes.String, - LogicalOperator = LogicalOperators.Eval, - Operator = Operators.Equal, - Operand = "USA" - } + RootCondition = new ValueConditionNode(DataTypes.String, ConditionType.IsoCountryCode.ToString(), Operators.Equal, "USA") } }; var dateBegin = new DateTime(2022, 01, 01); var dateEnd = new DateTime(2022, 12, 01); - var genericContentType = new GenericContentType { Identifier = "Type1" }; + var genericContentType = "Type1"; - var genericSearchArgs = new SearchArgs(genericContentType, dateBegin, dateEnd); + var genericSearchArgs = new SearchArgs(genericContentType, dateBegin, dateEnd); var testRules = new List> { - new Rule - { + new() { ContentContainer = new ContentContainer(ContentType.Type1, (_) => new object()), DateBegin = new DateTime(2018, 01, 01), DateEnd = new DateTime(2019, 01, 01), @@ -135,7 +123,7 @@ public async Task GenericRulesEngine_SearchAsync_Success() var genericRulesEngine = new GenericRulesEngine(this.mockRulesEngine.Object); // Act - var genericRules = await genericRulesEngine.SearchAsync(genericSearchArgs).ConfigureAwait(false); + var genericRules = await genericRulesEngine.SearchAsync(genericSearchArgs); // Assert genericRules.Should().BeEquivalentTo(expectedGenericRules); diff --git a/tests/Rules.Framework.WebUI.Tests/Extensions/RuleDtoExtensionsTests.cs b/tests/Rules.Framework.WebUI.Tests/Extensions/RuleDtoExtensionsTests.cs index ef9c4343..0fdecbd8 100644 --- a/tests/Rules.Framework.WebUI.Tests/Extensions/RuleDtoExtensionsTests.cs +++ b/tests/Rules.Framework.WebUI.Tests/Extensions/RuleDtoExtensionsTests.cs @@ -1,6 +1,9 @@ namespace Rules.Framework.WebUI.Tests.Extensions { + using System; + using System.Globalization; using FluentAssertions; + using Rules.Framework.Core; using Rules.Framework.Generics; using Rules.Framework.WebUI.Dto; using Rules.Framework.WebUI.Extensions; @@ -19,14 +22,17 @@ public RuleDtoExtensionsTests() public void RuleDtoExtensions_ToRuleDto_Success() { // Arrange - var genericRule = new GenericRule(); - var contentType = "contentType"; + var genericRule = RuleBuilder.NewRule() + .WithName("test rule") + .WithContent("contentType", new object()) + .WithDateBegin(DateTime.Parse("2024-01-01Z", CultureInfo.InvariantCulture, DateTimeStyles.AdjustToUniversal)) + .Build().Rule; // Act - var ruleDto = genericRule.ToRuleDto(contentType, this.ruleStatusDtoAnalyzer); + var ruleDto = genericRule.ToRuleDto(this.ruleStatusDtoAnalyzer); // Assert ruleDto.Should().NotBeNull(); ruleDto.Should().BeOfType(); } } -} +} \ No newline at end of file diff --git a/tests/Rules.Framework.WebUI.Tests/Handlers/GetRulesHandlerTests.cs b/tests/Rules.Framework.WebUI.Tests/Handlers/GetRulesHandlerTests.cs index 973c1f2b..68ade034 100644 --- a/tests/Rules.Framework.WebUI.Tests/Handlers/GetRulesHandlerTests.cs +++ b/tests/Rules.Framework.WebUI.Tests/Handlers/GetRulesHandlerTests.cs @@ -8,6 +8,7 @@ namespace Rules.Framework.WebUI.Tests.Handlers using FluentAssertions; using Microsoft.AspNetCore.Http; using Moq; + using Rules.Framework.Core; using Rules.Framework.Generics; using Rules.Framework.WebUI.Dto; using Rules.Framework.WebUI.Handlers; @@ -36,7 +37,7 @@ public async Task HandleRequestAsync_Validation(string httpMethod, string resour { //Arrange var httpContext = HttpContextHelper.CreateHttpContext(httpMethod, resourcePath); - var genericRule = new List(); + var genericRule = new List>(); var verifySearchAsync = false; if (statusCode == HttpStatusCode.OK || statusCode == HttpStatusCode.InternalServerError) @@ -47,13 +48,13 @@ public async Task HandleRequestAsync_Validation(string httpMethod, string resour if (statusCode == HttpStatusCode.OK) { - this.rulesEngine.Setup(d => d.SearchAsync(It.IsAny>())) + this.rulesEngine.Setup(d => d.SearchAsync(It.IsAny>())) .ReturnsAsync(genericRule); } if (statusCode == HttpStatusCode.InternalServerError) { - this.rulesEngine.Setup(d => d.SearchAsync(It.IsAny>())) + this.rulesEngine.Setup(d => d.SearchAsync(It.IsAny>())) .Throws(new Exception("message", new Exception("inner"))); } } @@ -65,8 +66,7 @@ public async Task HandleRequestAsync_Validation(string httpMethod, string resour //Act var result = await this.handler - .HandleAsync(httpContext.Request, httpContext.Response, next) - .ConfigureAwait(false); + .HandleAsync(httpContext.Request, httpContext.Response, next); //Assert result.Should().Be(expectedResult); @@ -88,13 +88,13 @@ public async Task HandleRequestAsync_Validation(string httpMethod, string resour if (verifySearchAsync) { this.rulesEngine - .Verify(s => s.SearchAsync(It.IsAny>()), Times.Once); + .Verify(s => s.SearchAsync(It.IsAny>()), Times.Once); } else { this.rulesEngine - .Verify(s => s.SearchAsync(It.IsAny>()), Times.Never); + .Verify(s => s.SearchAsync(It.IsAny>()), Times.Never); } } } -} +} \ No newline at end of file From 1786d1a65c184afdc3ff2b8b1e2172026c6d8fae Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Sun, 19 May 2024 15:52:52 +0100 Subject: [PATCH 3/9] chore: nuget packaging changes --- .github/workflows/dotnet-publish.yml | 3 +++ .../Rules.Framework.Rql.csproj | 21 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/.github/workflows/dotnet-publish.yml b/.github/workflows/dotnet-publish.yml index b18e3a27..b1e44e61 100644 --- a/.github/workflows/dotnet-publish.yml +++ b/.github/workflows/dotnet-publish.yml @@ -40,6 +40,9 @@ jobs: - name: Pack Rules.Framework.Providers.MongoDb run: dotnet pack src/Rules.Framework.Providers.MongoDb/Rules.Framework.Providers.MongoDb.csproj --include-symbols -c Release /p:Version=$BUILD_VERSION + - name: Pack Rules.Framework.Rql + run: dotnet pack src/Rules.Framework.Rql/Rules.Framework.Rql.csproj --include-symbols -c Release /p:Version=$BUILD_VERSION + - name: Pack Rules.Framework.WebUI run: dotnet pack src/Rules.Framework.WebUI/Rules.Framework.WebUI.csproj --include-symbols -c Release /p:Version=$BUILD_VERSION diff --git a/src/Rules.Framework.Rql/Rules.Framework.Rql.csproj b/src/Rules.Framework.Rql/Rules.Framework.Rql.csproj index 83478e36..05fc511e 100644 --- a/src/Rules.Framework.Rql/Rules.Framework.Rql.csproj +++ b/src/Rules.Framework.Rql/Rules.Framework.Rql.csproj @@ -3,8 +3,29 @@ netstandard2.0;netstandard2.1 10.0 + true + + + + + + + LICENSE.md + + + Git + rules rulesframework rql query language + A query languague implementation for rules framework - RQL. + + + + + True + + + From e95b4b91de4744c3e8845411ee46f15b988d75c3 Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Sun, 19 May 2024 22:10:06 +0100 Subject: [PATCH 4/9] fix: fix reverse rql builder issue with decimals conversion to string --- src/Rules.Framework.Rql/ReverseRqlBuilder.cs | 3 ++- tests/Rules.Framework.Rql.Tests/ReverseRqlBuilderTests.cs | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Rules.Framework.Rql/ReverseRqlBuilder.cs b/src/Rules.Framework.Rql/ReverseRqlBuilder.cs index 23c2c64f..018545b2 100644 --- a/src/Rules.Framework.Rql/ReverseRqlBuilder.cs +++ b/src/Rules.Framework.Rql/ReverseRqlBuilder.cs @@ -1,6 +1,7 @@ namespace Rules.Framework.Rql { using System; + using System.Globalization; using System.Linq; using System.Text; using Rules.Framework.Rql.Ast; @@ -91,7 +92,7 @@ public string VisitInputConditionsSegment(InputConditionsSegment inputConditions { LiteralType.String or LiteralType.Undefined => literalExpression.Token.Lexeme, LiteralType.Bool => literalExpression.Value.ToString().ToUpperInvariant(), - LiteralType.Decimal or LiteralType.Integer => literalExpression.Value.ToString(), + LiteralType.Decimal or LiteralType.Integer => Convert.ToString(literalExpression.Value, CultureInfo.InvariantCulture), LiteralType.DateTime => $"${literalExpression.Value:yyyy-MM-ddTHH:mm:ssZ}$", _ => throw new NotSupportedException($"The literal type '{literalExpression.Type}' is not supported."), }; diff --git a/tests/Rules.Framework.Rql.Tests/ReverseRqlBuilderTests.cs b/tests/Rules.Framework.Rql.Tests/ReverseRqlBuilderTests.cs index b7b070fd..da917e37 100644 --- a/tests/Rules.Framework.Rql.Tests/ReverseRqlBuilderTests.cs +++ b/tests/Rules.Framework.Rql.Tests/ReverseRqlBuilderTests.cs @@ -275,7 +275,7 @@ public void VisitKeywordExpression_GivenKeywordExpression_ReturnsRqlRepresentati [Theory] [InlineData(LiteralType.Bool, true, "TRUE")] - [InlineData(LiteralType.Decimal, 10.35, "10,35")] + [InlineData(LiteralType.Decimal, 10.35, "10.35")] [InlineData(LiteralType.Integer, 3, "3")] [InlineData(LiteralType.String, "test", "test")] [InlineData(LiteralType.DateTime, "2024-01-05T22:36:05Z", "$2024-01-05T22:36:05Z$")] From 2c6c46f8d56af1df4cf6bcd00d4b88a70d3328f3 Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Sun, 19 May 2024 23:35:19 +0100 Subject: [PATCH 5/9] chore: fix codacy code analysis issues --- .../Ast/Expressions/IExpressionVisitor.cs | 2 +- .../Ast/Segments/ISegmentVisitor.cs | 2 +- .../Ast/Statements/IStatementVisitor.cs | 2 +- .../Pipeline/Interpret/Interpreter.cs | 21 ++++++++++++------- .../Pipeline/Parse/IParseStrategy.cs | 2 +- .../Pipeline/Parse/PanicModeInfo.cs | 8 ++++++- .../Pipeline/Parse/Parser.cs | 11 ++++------ .../Pipeline/Scan/Scanner.cs | 15 +++++++------ src/Rules.Framework.Rql/RqlSourcePosition.cs | 6 +++++- .../Runtime/Types/RqlAny.cs | 12 ++++++----- .../Runtime/Types/RqlArray.cs | 20 +++++++++++++++++- .../Runtime/Types/RqlBool.cs | 6 ++++-- .../Runtime/Types/RqlDate.cs | 6 ++++-- .../Runtime/Types/RqlDecimal.cs | 6 ++++-- .../Runtime/Types/RqlInteger.cs | 6 ++++-- .../Runtime/Types/RqlNothing.cs | 6 ++++-- .../Runtime/Types/RqlObject.cs | 5 ++++- .../Runtime/Types/RqlReadOnlyObject.cs | 6 ++++-- .../Runtime/Types/RqlRule.cs | 9 ++++---- .../Runtime/Types/RqlString.cs | 6 ++++-- .../Runtime/Types/RqlType.cs | 4 +++- .../Extensions/RuleExtensions.cs | 4 ++-- .../RqlEngineTests.cs | 8 +++---- .../Runtime/RqlRuntimeTests.cs | 1 - .../Rules.Framework.RqlReplTester/Program.cs | 2 +- 25 files changed, 115 insertions(+), 61 deletions(-) diff --git a/src/Rules.Framework.Rql/Ast/Expressions/IExpressionVisitor.cs b/src/Rules.Framework.Rql/Ast/Expressions/IExpressionVisitor.cs index 78e7ae27..fd4b6588 100644 --- a/src/Rules.Framework.Rql/Ast/Expressions/IExpressionVisitor.cs +++ b/src/Rules.Framework.Rql/Ast/Expressions/IExpressionVisitor.cs @@ -1,6 +1,6 @@ namespace Rules.Framework.Rql.Ast.Expressions { - internal interface IExpressionVisitor + internal interface IExpressionVisitor { T VisitAssignmentExpression(AssignmentExpression assignmentExpression); diff --git a/src/Rules.Framework.Rql/Ast/Segments/ISegmentVisitor.cs b/src/Rules.Framework.Rql/Ast/Segments/ISegmentVisitor.cs index 637a4716..587213e9 100644 --- a/src/Rules.Framework.Rql/Ast/Segments/ISegmentVisitor.cs +++ b/src/Rules.Framework.Rql/Ast/Segments/ISegmentVisitor.cs @@ -1,6 +1,6 @@ namespace Rules.Framework.Rql.Ast.Segments { - internal interface ISegmentVisitor + internal interface ISegmentVisitor { T VisitCardinalitySegment(CardinalitySegment cardinalitySegment); diff --git a/src/Rules.Framework.Rql/Ast/Statements/IStatementVisitor.cs b/src/Rules.Framework.Rql/Ast/Statements/IStatementVisitor.cs index 61f3cbe1..77fb5ed4 100644 --- a/src/Rules.Framework.Rql/Ast/Statements/IStatementVisitor.cs +++ b/src/Rules.Framework.Rql/Ast/Statements/IStatementVisitor.cs @@ -1,6 +1,6 @@ namespace Rules.Framework.Rql.Ast.Statements { - internal interface IStatementVisitor + internal interface IStatementVisitor { T VisitExpressionStatement(ExpressionStatement expressionStatement); diff --git a/src/Rules.Framework.Rql/Pipeline/Interpret/Interpreter.cs b/src/Rules.Framework.Rql/Pipeline/Interpret/Interpreter.cs index 53958816..a26762f4 100644 --- a/src/Rules.Framework.Rql/Pipeline/Interpret/Interpreter.cs +++ b/src/Rules.Framework.Rql/Pipeline/Interpret/Interpreter.cs @@ -16,8 +16,8 @@ namespace Rules.Framework.Rql.Pipeline.Interpret internal class Interpreter : IInterpreter, IExpressionVisitor>, ISegmentVisitor>, IStatementVisitor> { private readonly IReverseRqlBuilder reverseRqlBuilder; + private readonly IRuntime runtime; private bool disposedValue; - private IRuntime runtime; public Interpreter( IRuntime runtime, @@ -258,15 +258,24 @@ public Task VisitOperatorSegment(OperatorSegment operatorExpression) case TokenType.STAR: resultOperator = RqlOperators.Star; break; + + default: + ThrowNotSupportedException(); + break; } if (resultOperator == RqlOperators.None) { - var tokenTypes = operatorExpression.Tokens.Select(t => $"'{t.Type}'").Aggregate((t1, t2) => $"{t1}, {t2}"); - throw new NotSupportedException($"The tokens with types [{tokenTypes}] are not supported as a valid operator."); + ThrowNotSupportedException(); } return Task.FromResult(resultOperator); + + void ThrowNotSupportedException() + { + var tokenTypes = operatorExpression.Tokens.Select(t => $"'{t.Type}'").Aggregate((t1, t2) => $"{t1}, {t2}"); + throw new NotSupportedException($"The tokens with types [{tokenTypes}] are not supported as a valid operator."); + } } public Task VisitPlaceholderExpression(PlaceholderExpression placeholderExpression) @@ -300,11 +309,7 @@ public async Task VisitUnaryExpression(UnaryExpression unaryExpre { try { - var @operator = unaryExpression.Operator.Lexeme switch - { - "-" => RqlOperators.Minus, - _ => RqlOperators.None, - }; + var @operator = unaryExpression.Operator.Lexeme is "-" ? RqlOperators.Minus : RqlOperators.None; var right = await unaryExpression.Right.Accept(this).ConfigureAwait(false); return this.runtime.ApplyUnary(right, @operator); } diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/IParseStrategy.cs b/src/Rules.Framework.Rql/Pipeline/Parse/IParseStrategy.cs index 280c8e78..be887748 100644 --- a/src/Rules.Framework.Rql/Pipeline/Parse/IParseStrategy.cs +++ b/src/Rules.Framework.Rql/Pipeline/Parse/IParseStrategy.cs @@ -1,6 +1,6 @@ namespace Rules.Framework.Rql.Pipeline.Parse { - internal interface IParseStrategy + internal interface IParseStrategy { TParseOutput Parse(ParseContext parseContext); } diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/PanicModeInfo.cs b/src/Rules.Framework.Rql/Pipeline/Parse/PanicModeInfo.cs index ca75ee94..9a769de7 100644 --- a/src/Rules.Framework.Rql/Pipeline/Parse/PanicModeInfo.cs +++ b/src/Rules.Framework.Rql/Pipeline/Parse/PanicModeInfo.cs @@ -1,10 +1,11 @@ namespace Rules.Framework.Rql.Pipeline.Parse { + using System; using System.Diagnostics.CodeAnalysis; using Rules.Framework.Rql.Tokens; [ExcludeFromCodeCoverage] - internal readonly struct PanicModeInfo + internal readonly struct PanicModeInfo : IEquatable { public static readonly PanicModeInfo None = new(causeToken: null!, message: null!); @@ -17,5 +18,10 @@ public PanicModeInfo(Token causeToken, string message) public Token CauseToken { get; } public string Message { get; } + + public bool Equals(PanicModeInfo other) + { + return this.CauseToken == other.CauseToken && string.Equals(this.Message, other.Message, StringComparison.Ordinal); + } } } \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Pipeline/Parse/Parser.cs b/src/Rules.Framework.Rql/Pipeline/Parse/Parser.cs index e131268f..f3de20a5 100644 --- a/src/Rules.Framework.Rql/Pipeline/Parse/Parser.cs +++ b/src/Rules.Framework.Rql/Pipeline/Parse/Parser.cs @@ -1,6 +1,7 @@ namespace Rules.Framework.Rql.Pipeline.Parse { using System.Collections.Generic; + using System.Linq; using Rules.Framework.Rql.Ast.Statements; using Rules.Framework.Rql.Messages; using Rules.Framework.Rql.Pipeline.Parse.Strategies; @@ -8,6 +9,7 @@ namespace Rules.Framework.Rql.Pipeline.Parse internal class Parser : IParser { + private static readonly TokenType[] synchronizableTokens = new[] { TokenType.SEMICOLON, TokenType.EOF }; private readonly IParseStrategyProvider parseStrategyProvider; public Parser(IParseStrategyProvider parseStrategyProvider) @@ -53,14 +55,9 @@ private static void Synchronize(ParseContext parseContext) { while (parseContext.MoveNext()) { - switch (parseContext.GetCurrentToken().Type) + if (synchronizableTokens.Contains(parseContext.GetCurrentToken().Type)) { - case TokenType.SEMICOLON: - case TokenType.EOF: - return; - - default: - break; + return; } } } diff --git a/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs b/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs index 76b95f82..192d1f3e 100644 --- a/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs +++ b/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs @@ -55,10 +55,6 @@ internal class Scanner : IScanner { nameof(TokenType.WITH), TokenType.WITH }, }; - public Scanner() - { - } - public ScanResult ScanTokens(string source) { if (source is null) @@ -108,7 +104,10 @@ public ScanResult ScanTokens(string source) private static void ConsumeAlphaNumeric(ScanContext scanContext) { - while (IsAlphaNumeric(scanContext.GetNextChar()) && scanContext.MoveNext()) { } + while (IsAlphaNumeric(scanContext.GetNextChar()) && scanContext.MoveNext()) + { + continue; + } } private static Token CreateToken(ScanContext scanContext, TokenType tokenType) @@ -135,6 +134,7 @@ private static Token HandleDate(ScanContext scanContext) string lexeme; while (scanContext.GetNextChar() != '$' && scanContext.MoveNext()) { + continue; } if (scanContext.IsEof()) @@ -200,7 +200,10 @@ private static Token HandleNumber(ScanContext scanContext) static void ConsumeDigits(ScanContext scanContext) { - while (IsNumeric(scanContext.GetNextChar()) && scanContext.MoveNext()) { } + while (IsNumeric(scanContext.GetNextChar()) && scanContext.MoveNext()) + { + continue; + } } static bool ConsumeRemainingTokenCharacters(ScanContext scanContext) diff --git a/src/Rules.Framework.Rql/RqlSourcePosition.cs b/src/Rules.Framework.Rql/RqlSourcePosition.cs index 27fcaa91..214e0fbc 100644 --- a/src/Rules.Framework.Rql/RqlSourcePosition.cs +++ b/src/Rules.Framework.Rql/RqlSourcePosition.cs @@ -1,9 +1,10 @@ namespace Rules.Framework.Rql { + using System; using System.Runtime.InteropServices; [StructLayout(LayoutKind.Sequential)] - public readonly struct RqlSourcePosition + public readonly struct RqlSourcePosition : IEquatable { private RqlSourcePosition(uint line, uint column) { @@ -20,5 +21,8 @@ private RqlSourcePosition(uint line, uint column) public static RqlSourcePosition From(uint line, uint column) => new RqlSourcePosition(line, column); public override string ToString() => $"{{{this.Line}:{this.Column}}}"; + + public bool Equals(RqlSourcePosition other) + => this.Line == other.Line && this.Column == other.Column; } } \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlAny.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlAny.cs index 074c53f5..76328de9 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlAny.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlAny.cs @@ -3,7 +3,7 @@ namespace Rules.Framework.Rql.Runtime.Types using System; using Rules.Framework.Rql.Runtime; - public readonly struct RqlAny : IRuntimeValue + public readonly struct RqlAny : IRuntimeValue, IEquatable { private static readonly RqlType type = RqlTypes.Any; @@ -16,13 +16,13 @@ public RqlAny() internal RqlAny(IRuntimeValue value) { - var underlyingRuntimeValue = value; - while (underlyingRuntimeValue is RqlAny rqlAny) + var runtimeValue = value; + while (runtimeValue is RqlAny rqlAny) { - underlyingRuntimeValue = rqlAny.Unwrap(); + runtimeValue = rqlAny.Unwrap(); } - this.underlyingRuntimeValue = underlyingRuntimeValue; + this.underlyingRuntimeValue = runtimeValue; } public Type RuntimeType => this.underlyingRuntimeValue.RuntimeType; @@ -35,6 +35,8 @@ internal RqlAny(IRuntimeValue value) public object Value => this.underlyingRuntimeValue.RuntimeValue; + public bool Equals(RqlAny other) => this.underlyingRuntimeValue == other.underlyingRuntimeValue; + public override string ToString() => $"<{this.Type.Name}> ({this.underlyingRuntimeValue.ToString()})"; diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlArray.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlArray.cs index fcf2ff8f..6b444e11 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlArray.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlArray.cs @@ -5,7 +5,7 @@ namespace Rules.Framework.Rql.Runtime.Types using System.Text; using Rules.Framework.Rql.Runtime; - public readonly struct RqlArray : IRuntimeValue + public readonly struct RqlArray : IRuntimeValue, IEquatable { private static readonly Type runtimeType = typeof(object[]); private static readonly RqlType type = RqlTypes.Array; @@ -56,6 +56,24 @@ public static object[] ConvertToNativeArray(RqlArray rqlArray) public static implicit operator RqlAny(RqlArray rqlArray) => new RqlAny(rqlArray); + public bool Equals(RqlArray other) + { + if (this.Size != other.Size) + { + return false; + } + + for (int i = 0; i < this.size; i++) + { + if (!this.Value[i].Equals(other.Value[i])) + { + return false; + } + } + + return true; + } + public RqlNothing SetAtIndex(RqlInteger index, RqlAny value) { if (index.Value < 0 || index.Value >= this.size) diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlBool.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlBool.cs index 341aa30e..be1db973 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlBool.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlBool.cs @@ -3,7 +3,7 @@ namespace Rules.Framework.Rql.Runtime.Types using System; using Rules.Framework.Rql.Runtime; - public readonly struct RqlBool : IRuntimeValue + public readonly struct RqlBool : IRuntimeValue, IEquatable { private static readonly Type runtimeType = typeof(bool); private static readonly RqlType type = RqlTypes.Bool; @@ -27,7 +27,9 @@ internal RqlBool(bool value) public static implicit operator RqlBool(bool value) => new RqlBool(value); + public bool Equals(RqlBool other) => this.Value == other.Value; + public override string ToString() - => $"<{Type.Name}> {this.Value}"; + => $"<{Type.Name}> {this.Value}"; } } \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlDate.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlDate.cs index 5fee8994..d1b5a177 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlDate.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlDate.cs @@ -3,7 +3,7 @@ namespace Rules.Framework.Rql.Runtime.Types using System; using Rules.Framework.Rql.Runtime; - public readonly struct RqlDate : IRuntimeValue + public readonly struct RqlDate : IRuntimeValue, IEquatable { private static readonly Type runtimeType = typeof(DateTime); private static readonly RqlType type = RqlTypes.Date; @@ -27,7 +27,9 @@ internal RqlDate(DateTime value) public static implicit operator RqlDate(DateTime value) => new RqlDate(value); + public bool Equals(RqlDate other) => this.Value == other.Value; + public override string ToString() - => $"<{Type.Name}> {this.Value:g}"; + => $"<{Type.Name}> {this.Value:g}"; } } \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlDecimal.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlDecimal.cs index 49a7eb4b..4a4f9ccf 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlDecimal.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlDecimal.cs @@ -3,7 +3,7 @@ namespace Rules.Framework.Rql.Runtime.Types using System; using Rules.Framework.Rql.Runtime; - public readonly struct RqlDecimal : IRuntimeValue + public readonly struct RqlDecimal : IRuntimeValue, IEquatable { private static readonly Type runtimeType = typeof(decimal); private static readonly RqlType type = RqlTypes.Decimal; @@ -27,7 +27,9 @@ internal RqlDecimal(decimal value) public static implicit operator RqlDecimal(decimal value) => new RqlDecimal(value); + public bool Equals(RqlDecimal other) => this.Value == other.Value; + public override string ToString() - => $"<{Type.Name}> {this.Value}"; + => $"<{Type.Name}> {this.Value}"; } } \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlInteger.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlInteger.cs index 18f1707b..306f2f44 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlInteger.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlInteger.cs @@ -3,7 +3,7 @@ namespace Rules.Framework.Rql.Runtime.Types using System; using Rules.Framework.Rql.Runtime; - public readonly struct RqlInteger : IRuntimeValue + public readonly struct RqlInteger : IRuntimeValue, IEquatable { private static readonly Type runtimeType = typeof(int); private static readonly RqlType type = RqlTypes.Integer; @@ -27,7 +27,9 @@ internal RqlInteger(int value) public static implicit operator RqlInteger(int value) => new RqlInteger(value); + public bool Equals(RqlInteger other) => this.Value == other.Value; + public override string ToString() - => $"<{Type.Name}> {this.Value}"; + => $"<{Type.Name}> {this.Value}"; } } \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlNothing.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlNothing.cs index 809779cd..3909fc05 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlNothing.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlNothing.cs @@ -3,7 +3,7 @@ namespace Rules.Framework.Rql.Runtime.Types using System; using Rules.Framework.Rql.Runtime; - public readonly struct RqlNothing : IRuntimeValue + public readonly struct RqlNothing : IRuntimeValue, IEquatable { private static readonly Type runtimeType = typeof(object); private static readonly RqlType type = RqlTypes.Nothing; @@ -15,7 +15,9 @@ namespace Rules.Framework.Rql.Runtime.Types public static implicit operator RqlAny(RqlNothing rqlNothing) => new RqlAny(rqlNothing); + public bool Equals(RqlNothing other) => true; + public override string ToString() - => $"<{Type.Name}>"; + => $"<{Type.Name}>"; } } \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlObject.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlObject.cs index 76961f0a..2d872f85 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlObject.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlObject.cs @@ -5,7 +5,7 @@ namespace Rules.Framework.Rql.Runtime.Types using System.Text; using Rules.Framework.Rql.Runtime; - public readonly struct RqlObject : IRuntimeValue, IPropertySet + public readonly struct RqlObject : IRuntimeValue, IPropertySet, IEquatable { private static readonly Type runtimeType = typeof(object); private static readonly RqlType type = RqlTypes.Object; @@ -26,6 +26,9 @@ public RqlObject() public static implicit operator RqlAny(RqlObject rqlObject) => new RqlAny(rqlObject); + public bool Equals(RqlObject other) + => this.properties.Equals(other.properties); + public RqlAny SetPropertyValue(RqlString name, RqlAny value) => this.properties[name.Value] = value; public override string ToString() diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlReadOnlyObject.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlReadOnlyObject.cs index c215802c..481ed1e2 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlReadOnlyObject.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlReadOnlyObject.cs @@ -4,7 +4,7 @@ namespace Rules.Framework.Rql.Runtime.Types using System.Collections.Generic; using System.Text; - public readonly struct RqlReadOnlyObject : IRuntimeValue + public readonly struct RqlReadOnlyObject : IRuntimeValue, IEquatable { private static readonly Type runtimeType = typeof(object); private static readonly RqlType type = RqlTypes.ReadOnlyObject; @@ -25,8 +25,10 @@ internal RqlReadOnlyObject(IDictionary properties) public static implicit operator RqlAny(RqlReadOnlyObject rqlReadOnlyObject) => new RqlAny(rqlReadOnlyObject); + public bool Equals(RqlReadOnlyObject other) => this.properties.Equals(other.properties); + public override string ToString() - => $"<{Type.Name}>{Environment.NewLine}{this.ToString(4)}"; + => $"<{Type.Name}>{Environment.NewLine}{this.ToString(4)}"; internal string ToString(int indent) { diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlRule.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlRule.cs index e77c75d2..83f4d3f3 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlRule.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlRule.cs @@ -7,10 +7,9 @@ namespace Rules.Framework.Rql.Runtime.Types using Rules.Framework.Core; using Rules.Framework.Core.ConditionNodes; - public readonly struct RqlRule : IRuntimeValue + public readonly struct RqlRule : IRuntimeValue, IEquatable> { private static readonly Type runtimeType = typeof(Rule); - private static readonly RqlType type = RqlTypes.Rule; private readonly Dictionary properties; internal RqlRule(Rule rule) @@ -31,14 +30,16 @@ internal RqlRule(Rule rule) public object RuntimeValue => this.Value; - public RqlType Type => type; + public RqlType Type => RqlTypes.Rule; public readonly Rule Value { get; } public static implicit operator RqlAny(RqlRule rqlRule) => new RqlAny(rqlRule); + public bool Equals(RqlRule other) => this.Value.Equals(other.Value); + public override string ToString() - => $"<{Type.Name}>{Environment.NewLine}{this.ToString(4)}"; + => $"<{Type.Name}>{Environment.NewLine}{this.ToString(4)}"; internal string ToString(int indent) { diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlString.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlString.cs index 17cff583..396c06c9 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlString.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlString.cs @@ -4,7 +4,7 @@ namespace Rules.Framework.Rql.Runtime.Types using System.Collections.Generic; using Rules.Framework.Rql.Runtime; - public readonly struct RqlString : IRuntimeValue + public readonly struct RqlString : IRuntimeValue, IEquatable { private static readonly Type runtimeType = typeof(string); private static readonly RqlType type = RqlTypes.String; @@ -28,7 +28,9 @@ internal RqlString(string value) public static implicit operator string(RqlString rqlString) => rqlString.Value; + public bool Equals(RqlString other) => this.Value == other.Value; + public override string ToString() - => @$"<{Type.Name}> ""{this.Value}"""; + => @$"<{Type.Name}> ""{this.Value}"""; } } \ No newline at end of file diff --git a/src/Rules.Framework.Rql/Runtime/Types/RqlType.cs b/src/Rules.Framework.Rql/Runtime/Types/RqlType.cs index 542250b5..e80791e7 100644 --- a/src/Rules.Framework.Rql/Runtime/Types/RqlType.cs +++ b/src/Rules.Framework.Rql/Runtime/Types/RqlType.cs @@ -3,7 +3,7 @@ namespace Rules.Framework.Rql.Runtime.Types using System; using System.Collections.Generic; - public readonly struct RqlType + public readonly struct RqlType : IEquatable { private readonly IDictionary assignableTypes; @@ -26,6 +26,8 @@ public RqlType(string name) public static bool operator ==(RqlType left, RqlType right) => string.Equals(left.Name, right.Name, StringComparison.Ordinal); + public bool Equals(RqlType other) => string.Equals(this.Name, other.Name, StringComparison.Ordinal); + public bool IsAssignableTo(RqlType rqlType) { if (string.Equals(rqlType.Name, this.Name, StringComparison.Ordinal)) diff --git a/src/Rules.Framework/Extensions/RuleExtensions.cs b/src/Rules.Framework/Extensions/RuleExtensions.cs index 5008e07b..52eee77b 100644 --- a/src/Rules.Framework/Extensions/RuleExtensions.cs +++ b/src/Rules.Framework/Extensions/RuleExtensions.cs @@ -52,7 +52,7 @@ public static Rule ToConcreteRule()!, ContentContainer = new ContentContainer( rule.ContentContainer.ContentType.ToConcreteContentType(), - (t) => rule.ContentContainer.GetContentAs()), + (_) => rule.ContentContainer.GetContentAs()), DateBegin = rule.DateBegin, DateEnd = rule.DateEnd, Name = rule.Name, @@ -94,7 +94,7 @@ public static Rule ToGenericRule(t RootCondition = rule.RootCondition?.ToGenericConditionNode()!, ContentContainer = new ContentContainer( rule.ContentContainer.ContentType!.ToString(), - (t) => rule.ContentContainer.GetContentAs()), + (_) => rule.ContentContainer.GetContentAs()), DateBegin = rule.DateBegin, DateEnd = rule.DateEnd, Name = rule.Name, diff --git a/tests/Rules.Framework.Rql.Tests/RqlEngineTests.cs b/tests/Rules.Framework.Rql.Tests/RqlEngineTests.cs index e7187564..ef54e2a6 100644 --- a/tests/Rules.Framework.Rql.Tests/RqlEngineTests.cs +++ b/tests/Rules.Framework.Rql.Tests/RqlEngineTests.cs @@ -19,10 +19,10 @@ namespace Rules.Framework.Rql.Tests public class RqlEngineTests { - private IInterpreter interpreter; - private IParser parser; - private RqlEngine rqlEngine; - private IScanner scanner; + private readonly IInterpreter interpreter; + private readonly IParser parser; + private readonly RqlEngine rqlEngine; + private readonly IScanner scanner; public RqlEngineTests() { diff --git a/tests/Rules.Framework.Rql.Tests/Runtime/RqlRuntimeTests.cs b/tests/Rules.Framework.Rql.Tests/Runtime/RqlRuntimeTests.cs index e3c05f98..0ed37c1c 100644 --- a/tests/Rules.Framework.Rql.Tests/Runtime/RqlRuntimeTests.cs +++ b/tests/Rules.Framework.Rql.Tests/Runtime/RqlRuntimeTests.cs @@ -244,7 +244,6 @@ public async Task MatchRulesAsync_GivenOneMatchCardinalityWithoutResult_ReturnsE MatchDate = matchDate, }; - var expectedRule = BuildRule("Rule 1", DateTime.Parse("2024-01-01Z"), DateTime.Parse("2025-01-01Z"), new object(), contentType); var rulesEngine = Mock.Of>(); Mock.Get(rulesEngine) .Setup(x => x.MatchOneAsync(contentType, matchDate.Value, It.Is>>(c => c.SequenceEqual(conditions)))) diff --git a/tests/Rules.Framework.RqlReplTester/Program.cs b/tests/Rules.Framework.RqlReplTester/Program.cs index 36565438..216f4fe5 100644 --- a/tests/Rules.Framework.RqlReplTester/Program.cs +++ b/tests/Rules.Framework.RqlReplTester/Program.cs @@ -8,7 +8,7 @@ namespace Rules.Framework.RqlReplTester using Rules.Framework.Rql; using Rules.Framework.Rql.Runtime.Types; - internal class Program + internal static class Program { private static readonly ConsoleColor originalConsoleForegroundColor = Console.ForegroundColor; private static readonly string tab = new string(' ', 4); From 8693017c89d54fb8cf6f4d1b3c191e61e676104d Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Wed, 5 Jun 2024 22:48:11 +0100 Subject: [PATCH 6/9] chore: fix codacy code analysis issues --- src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs b/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs index 192d1f3e..64514198 100644 --- a/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs +++ b/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs @@ -104,9 +104,12 @@ public ScanResult ScanTokens(string source) private static void ConsumeAlphaNumeric(ScanContext scanContext) { - while (IsAlphaNumeric(scanContext.GetNextChar()) && scanContext.MoveNext()) + while (IsAlphaNumeric(scanContext.GetNextChar())) { - continue; + if (!scanContext.MoveNext()) + { + break; + } } } From 2b204f31df094c21e639425f237be340033a34bc Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Wed, 5 Jun 2024 23:02:49 +0100 Subject: [PATCH 7/9] chore: fix codacy code analysis issues --- src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs b/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs index 64514198..7be2bb2f 100644 --- a/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs +++ b/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs @@ -135,9 +135,12 @@ private static Token CreateToken(ScanContext scanContext, string lexeme, TokenTy private static Token HandleDate(ScanContext scanContext) { string lexeme; - while (scanContext.GetNextChar() != '$' && scanContext.MoveNext()) + while (scanContext.GetNextChar() != '$') { - continue; + if (!scanContext.MoveNext()) + { + break; + } } if (scanContext.IsEof()) @@ -205,7 +208,10 @@ static void ConsumeDigits(ScanContext scanContext) { while (IsNumeric(scanContext.GetNextChar()) && scanContext.MoveNext()) { - continue; + if (!scanContext.MoveNext()) + { + break; + } } } From 94662f46bc0ab7fddadbe6e03b575ba3d6a87108 Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Wed, 5 Jun 2024 23:07:53 +0100 Subject: [PATCH 8/9] fix: resolve bug scanning number tokens --- src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs b/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs index 7be2bb2f..4c6a2d07 100644 --- a/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs +++ b/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs @@ -206,7 +206,7 @@ private static Token HandleNumber(ScanContext scanContext) static void ConsumeDigits(ScanContext scanContext) { - while (IsNumeric(scanContext.GetNextChar()) && scanContext.MoveNext()) + while (IsNumeric(scanContext.GetNextChar())) { if (!scanContext.MoveNext()) { From 7a09d741461999a1b045941f0acdbc65ccfc114f Mon Sep 17 00:00:00 2001 From: Luis Garces Date: Sat, 22 Jun 2024 17:52:51 +0100 Subject: [PATCH 9/9] chore: code review changes --- .../Scan/{IScanner.cs => ITokenScanner.cs} | 2 +- .../Pipeline/Scan/ScanContext.cs | 8 ++-- .../Scan/{Scanner.cs => TokenScanner.cs} | 2 +- src/Rules.Framework.Rql/RqlEngine.cs | 8 ++-- src/Rules.Framework.Rql/RqlEngineArgs.cs | 2 +- src/Rules.Framework.Rql/RqlEngineBuilder.cs | 4 +- .../GrammarCheck/GrammarCheckTests.cs | 6 +-- .../RqlEngineTests.cs | 38 +++++++++---------- 8 files changed, 35 insertions(+), 35 deletions(-) rename src/Rules.Framework.Rql/Pipeline/Scan/{IScanner.cs => ITokenScanner.cs} (73%) rename src/Rules.Framework.Rql/Pipeline/Scan/{Scanner.cs => TokenScanner.cs} (99%) diff --git a/src/Rules.Framework.Rql/Pipeline/Scan/IScanner.cs b/src/Rules.Framework.Rql/Pipeline/Scan/ITokenScanner.cs similarity index 73% rename from src/Rules.Framework.Rql/Pipeline/Scan/IScanner.cs rename to src/Rules.Framework.Rql/Pipeline/Scan/ITokenScanner.cs index bcd93a8a..273cd91f 100644 --- a/src/Rules.Framework.Rql/Pipeline/Scan/IScanner.cs +++ b/src/Rules.Framework.Rql/Pipeline/Scan/ITokenScanner.cs @@ -1,6 +1,6 @@ namespace Rules.Framework.Rql.Pipeline.Scan { - internal interface IScanner + internal interface ITokenScanner { ScanResult ScanTokens(string source); } diff --git a/src/Rules.Framework.Rql/Pipeline/Scan/ScanContext.cs b/src/Rules.Framework.Rql/Pipeline/Scan/ScanContext.cs index bbb1f400..641dd2d6 100644 --- a/src/Rules.Framework.Rql/Pipeline/Scan/ScanContext.cs +++ b/src/Rules.Framework.Rql/Pipeline/Scan/ScanContext.cs @@ -86,11 +86,11 @@ private void DiscardTokenCandidate() this.TokenCandidate = null; } - private bool Move(int toOffset) + private bool Move(int offset) { - if (toOffset >= 0 && toOffset < this.source.Length) + if (offset >= 0 && offset < this.source.Length) { - var toChar = this.source[toOffset]; + var toChar = this.source[offset]; if (toChar == '\n') { this.NextLine(); @@ -100,7 +100,7 @@ private bool Move(int toOffset) this.NextColumn(); } - this.Offset = toOffset; + this.Offset = offset; return true; } diff --git a/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs b/src/Rules.Framework.Rql/Pipeline/Scan/TokenScanner.cs similarity index 99% rename from src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs rename to src/Rules.Framework.Rql/Pipeline/Scan/TokenScanner.cs index 4c6a2d07..859165b5 100644 --- a/src/Rules.Framework.Rql/Pipeline/Scan/Scanner.cs +++ b/src/Rules.Framework.Rql/Pipeline/Scan/TokenScanner.cs @@ -7,7 +7,7 @@ namespace Rules.Framework.Rql.Pipeline.Scan using Rules.Framework.Rql.Messages; using Rules.Framework.Rql.Tokens; - internal class Scanner : IScanner + internal class TokenScanner : ITokenScanner { private const char DecimalSeparator = '.'; diff --git a/src/Rules.Framework.Rql/RqlEngine.cs b/src/Rules.Framework.Rql/RqlEngine.cs index 17e46a3e..3b65ec66 100644 --- a/src/Rules.Framework.Rql/RqlEngine.cs +++ b/src/Rules.Framework.Rql/RqlEngine.cs @@ -17,11 +17,11 @@ internal class RqlEngine : IRqlEngine private bool disposedValue; private IInterpreter interpreter; private IParser parser; - private IScanner scanner; + private ITokenScanner tokenScanner; public RqlEngine(RqlEngineArgs rqlEngineArgs) { - this.scanner = rqlEngineArgs.Scanner; + this.tokenScanner = rqlEngineArgs.TokenScanner; this.parser = rqlEngineArgs.Parser; this.interpreter = rqlEngineArgs.Interpreter; } @@ -34,7 +34,7 @@ public void Dispose() public async Task> ExecuteAsync(string rql) { - var scanResult = this.scanner.ScanTokens(rql); + var scanResult = this.tokenScanner.ScanTokens(rql); if (!scanResult.Success) { var errors = scanResult.Messages.Where(m => m.Severity == MessageSeverity.Error) @@ -73,7 +73,7 @@ protected virtual void Dispose(bool disposing) if (disposing) { this.interpreter = null!; - this.scanner = null!; + this.tokenScanner = null!; this.parser = null!; } diff --git a/src/Rules.Framework.Rql/RqlEngineArgs.cs b/src/Rules.Framework.Rql/RqlEngineArgs.cs index 2788320b..5a230c92 100644 --- a/src/Rules.Framework.Rql/RqlEngineArgs.cs +++ b/src/Rules.Framework.Rql/RqlEngineArgs.cs @@ -14,6 +14,6 @@ internal class RqlEngineArgs public IParser Parser { get; set; } - public IScanner Scanner { get; set; } + public ITokenScanner TokenScanner { get; set; } } } \ No newline at end of file diff --git a/src/Rules.Framework.Rql/RqlEngineBuilder.cs b/src/Rules.Framework.Rql/RqlEngineBuilder.cs index 1f1831f7..83053697 100644 --- a/src/Rules.Framework.Rql/RqlEngineBuilder.cs +++ b/src/Rules.Framework.Rql/RqlEngineBuilder.cs @@ -30,7 +30,7 @@ public static RqlEngineBuilder CreateRqlEngine(IRu public IRqlEngine Build() { var runtime = RqlRuntime.Create(this.rulesEngine); - var scanner = new Scanner(); + var tokenScanner = new TokenScanner(); var parseStrategyProvider = new ParseStrategyPool(); var parser = new Parser(parseStrategyProvider); var reverseRqlBuilder = new ReverseRqlBuilder(); @@ -40,7 +40,7 @@ public IRqlEngine Build() Interpreter = interpreter, Options = this.options, Parser = parser, - Scanner = scanner, + TokenScanner = tokenScanner, }; return new RqlEngine(args); diff --git a/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/GrammarCheckTests.cs b/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/GrammarCheckTests.cs index b30ab6b3..99fe71bd 100644 --- a/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/GrammarCheckTests.cs +++ b/tests/Rules.Framework.Rql.IntegrationTests/GrammarCheck/GrammarCheckTests.cs @@ -20,12 +20,12 @@ public class GrammarCheckTests ]; private readonly IParser parser; - private readonly IScanner scanner; private readonly ITestOutputHelper testOutputHelper; + private readonly ITokenScanner tokenScanner; public GrammarCheckTests(ITestOutputHelper testOutputHelper) { - this.scanner = new Scanner(); + this.tokenScanner = new TokenScanner(); this.parser = new Parser(new ParseStrategyPool()); this.testOutputHelper = testOutputHelper; } @@ -111,7 +111,7 @@ public void CheckRqlGrammar(string rqlSource, bool expectsSuccess, IEnumerable errorMessages) { - var scanResult = this.scanner.ScanTokens(rqlSource); + var scanResult = this.tokenScanner.ScanTokens(rqlSource); if (!scanResult.Success) { errorMessages = scanResult.Messages.Select(x => x.Text).ToArray(); diff --git a/tests/Rules.Framework.Rql.Tests/RqlEngineTests.cs b/tests/Rules.Framework.Rql.Tests/RqlEngineTests.cs index ef54e2a6..73c9336f 100644 --- a/tests/Rules.Framework.Rql.Tests/RqlEngineTests.cs +++ b/tests/Rules.Framework.Rql.Tests/RqlEngineTests.cs @@ -22,18 +22,18 @@ public class RqlEngineTests private readonly IInterpreter interpreter; private readonly IParser parser; private readonly RqlEngine rqlEngine; - private readonly IScanner scanner; + private readonly ITokenScanner tokenScanner; public RqlEngineTests() { - this.scanner = Mock.Of(); + this.tokenScanner = Mock.Of(); this.parser = Mock.Of(); this.interpreter = Mock.Of(); var rqlEngineArgs = new RqlEngineArgs { Interpreter = interpreter, Parser = parser, - Scanner = scanner, + TokenScanner = tokenScanner, }; this.rqlEngine = new RqlEngine(rqlEngineArgs); @@ -103,7 +103,7 @@ public async Task ExecuteAsync_GivenRqlSourceCase1_InterpretsAndReturnsResult() interpretResult.AddStatementResult(new NothingStatementResult("MATCH ONE RULE FOR \"Test\" ON $2023-01-01Z$;")); interpretResult.AddStatementResult(new ExpressionStatementResult("MATCH ONE RULE FOR \"Other\\nTest\" ON $2024-01-01Z$;", rqlArray)); - Mock.Get(this.scanner) + Mock.Get(this.tokenScanner) .Setup(x => x.ScanTokens(It.Is(rql, StringComparer.Ordinal))) .Returns(scanResult); Mock.Get(this.parser) @@ -118,7 +118,7 @@ public async Task ExecuteAsync_GivenRqlSourceCase1_InterpretsAndReturnsResult() // Assert Mock.VerifyAll( - Mock.Get(this.scanner), + Mock.Get(this.tokenScanner), Mock.Get(this.parser), Mock.Get(this.interpreter)); @@ -172,7 +172,7 @@ public async Task ExecuteAsync_GivenRqlSourceCase2_InterpretsAndReturnsResult() var interpretResult = new InterpretResult(); interpretResult.AddStatementResult(new ExpressionStatementResult(rql, rqlArray)); - Mock.Get(this.scanner) + Mock.Get(this.tokenScanner) .Setup(x => x.ScanTokens(It.Is(rql, StringComparer.Ordinal))) .Returns(scanResult); Mock.Get(this.parser) @@ -187,7 +187,7 @@ public async Task ExecuteAsync_GivenRqlSourceCase2_InterpretsAndReturnsResult() // Assert Mock.VerifyAll( - Mock.Get(this.scanner), + Mock.Get(this.tokenScanner), Mock.Get(this.parser), Mock.Get(this.interpreter)); @@ -237,7 +237,7 @@ public async Task ExecuteAsync_GivenRqlSourceCase3_InterpretsAndReturnsResult() var interpretResult = new InterpretResult(); interpretResult.AddStatementResult(new ExpressionStatementResult(rql, rqlArray)); - Mock.Get(this.scanner) + Mock.Get(this.tokenScanner) .Setup(x => x.ScanTokens(It.Is(rql, StringComparer.Ordinal))) .Returns(scanResult); Mock.Get(this.parser) @@ -252,7 +252,7 @@ public async Task ExecuteAsync_GivenRqlSourceCase3_InterpretsAndReturnsResult() // Assert Mock.VerifyAll( - Mock.Get(this.scanner), + Mock.Get(this.tokenScanner), Mock.Get(this.parser), Mock.Get(this.interpreter)); @@ -294,7 +294,7 @@ public async Task ExecuteAsync_GivenRqlSourceCase4_InterpretsAndReturnsResult() var interpretResult = new InterpretResult(); interpretResult.AddStatementResult(new ExpressionStatementResult(rql, rqlString)); - Mock.Get(this.scanner) + Mock.Get(this.tokenScanner) .Setup(x => x.ScanTokens(It.Is(rql, StringComparer.Ordinal))) .Returns(scanResult); Mock.Get(this.parser) @@ -309,7 +309,7 @@ public async Task ExecuteAsync_GivenRqlSourceCase4_InterpretsAndReturnsResult() // Assert Mock.VerifyAll( - Mock.Get(this.scanner), + Mock.Get(this.tokenScanner), Mock.Get(this.parser), Mock.Get(this.interpreter)); @@ -340,7 +340,7 @@ public async Task ExecuteAsync_GivenRqlSourceCase5HavingErrorsOnScanner_ThrowsRq }; var scanResult = ScanResult.CreateError(messages); - Mock.Get(this.scanner) + Mock.Get(this.tokenScanner) .Setup(x => x.ScanTokens(It.Is(rql, StringComparer.Ordinal))) .Returns(scanResult); @@ -349,7 +349,7 @@ public async Task ExecuteAsync_GivenRqlSourceCase5HavingErrorsOnScanner_ThrowsRq // Assert Mock.VerifyAll( - Mock.Get(this.scanner)); + Mock.Get(this.tokenScanner)); rqlException.Message.Should().Be("Errors have occurred processing provided RQL source - Sample scan error for source @{1:1}-{1:10}"); rqlException.Errors.Should().HaveCount(1); @@ -380,7 +380,7 @@ public async Task ExecuteAsync_GivenRqlSourceCase6HavingErrorsOnParser_ThrowsRql }; var parseResult = ParseResult.CreateError(messages); - Mock.Get(this.scanner) + Mock.Get(this.tokenScanner) .Setup(x => x.ScanTokens(It.Is(rql, StringComparer.Ordinal))) .Returns(scanResult); Mock.Get(this.parser) @@ -392,7 +392,7 @@ public async Task ExecuteAsync_GivenRqlSourceCase6HavingErrorsOnParser_ThrowsRql // Assert Mock.VerifyAll( - Mock.Get(this.scanner), + Mock.Get(this.tokenScanner), Mock.Get(this.parser)); rqlException.Message.Should().Be("Errors have occurred processing provided RQL source - Sample parse error for source @{1:1}-{1:10}"); @@ -427,7 +427,7 @@ public async Task ExecuteAsync_GivenRqlSourceCase7HavingErrorsOnInterpreter_Thro var interpretResult = new InterpretResult(); interpretResult.AddStatementResult(new ErrorStatementResult("Sample interpret error", rql, RqlSourcePosition.From(1, 1), RqlSourcePosition.From(1, 10))); - Mock.Get(this.scanner) + Mock.Get(this.tokenScanner) .Setup(x => x.ScanTokens(It.Is(rql, StringComparer.Ordinal))) .Returns(scanResult); Mock.Get(this.parser) @@ -442,7 +442,7 @@ public async Task ExecuteAsync_GivenRqlSourceCase7HavingErrorsOnInterpreter_Thro // Assert Mock.VerifyAll( - Mock.Get(this.scanner), + Mock.Get(this.tokenScanner), Mock.Get(this.parser), Mock.Get(this.interpreter)); @@ -478,7 +478,7 @@ public async Task ExecuteAsync_GivenRqlSourceCase8HavingUnknownResultType_Throws var interpretResult = new InterpretResult(); interpretResult.AddStatementResult(new StubResult()); - Mock.Get(this.scanner) + Mock.Get(this.tokenScanner) .Setup(x => x.ScanTokens(It.Is(rql, StringComparer.Ordinal))) .Returns(scanResult); Mock.Get(this.parser) @@ -493,7 +493,7 @@ public async Task ExecuteAsync_GivenRqlSourceCase8HavingUnknownResultType_Throws // Assert Mock.VerifyAll( - Mock.Get(this.scanner), + Mock.Get(this.tokenScanner), Mock.Get(this.parser), Mock.Get(this.interpreter));